Разделение строки на массив слов

от автора

Вводная часть

Здравствуйте. Гуляя по просторам интернета, не раз встречал задания вроде такого

Дана строка, содержащая текст на русском языке. Подсчитать количество слов, начинающихся и заканчивающихся на одну и ту же букву

Все бы очень просто, но люди начинают пытаться воспользоваться встроенными методами, вроде strtok и string.Split(). Но они вечно забывают, что существуют небольшие нюансы использования подобных функций/методов…

Симптоматика

Больной не понимает, что использование разделителей не самая хорошая идея, в лучшем случае, он создает массив разделителей, и пихает его во все вышеописанные методы, в худшем — он в каждом случае пишет отдельно что-то вроде

var a = str.Split(new[] {",","."}, StringSplitOptions.RemoveEmptyEntries) 

соответственно при необходимости добавления еще одного символа требуется по всему коду лазить и исправлять эти Split’ы.

Лечение

Просьба использовать больше 50 символов-разделителей, тогда до больного начинает доходить необходимость в смене алгоритма.

Лекарство

В качестве лекарства мной был написан вот такой метод расширения:

using System; using System.Collections.Generic; using System.Linq; using System.Text;   namespace StrSplit {     public static class Program     {         public static IEnumerable<string> MySplit(this string s)         {             var sb = new StringBuilder();             foreach (char c in s)                 if (char.IsLetter(c))                     sb.Append(c);                 else                 {                     yield return sb.ToString();                     sb.Length = 0;                 }         } }

потом, посмотрев на встроенный StringSplit, решил добавить опцию, аналогичную StringSplitOptions.RemoveEmptyEntries
Итоговый вид метода

private static IEnumerable<string> MySplit(this string s, bool removeEmpty) {     var sb = new StringBuilder();     foreach (char c in s)     if (char.IsLetter(c))          sb.Append(c);     else     {          if (sb.Length != 0 || !removeEmpty)               yield return sb.ToString();          sb.Length = 0;      } }

Принцип действия

В принципе, все элементарно, создается объект StringBuilder (особый вид string в C#, позволяющий безболезненно суммировать большое кол-во строк, особенности реализации на конкретном языке, поэтому не будем останавливаться на этом), после этого он заполняется буквами до тех пор, пока не встретится не-буква, то есть в этом и проявляется суть метода, он работает с любыми разделителями, и нет необходимости исправлять программу в случае изменения каких-либо внешних факторов, достаточно указать только полезную информацию (в данном случае — буквы), и все. Для большей универсальности в качестве еще одного параметра можно передавать делегат (указатель) на функцию, которая будет проводить сравнение (то есть какое условие определяет, полезная буква или нет, добавлять её в вывод или удалить). Ну если встретился не-символ тогда проверяем: если строка пустая и мы не хотим получать пустую строку (параметр removeEmpty == true), тогда просто обнуляем текущую строку иначе возвращаем полученный результат.

Небольшой аналог на C

typedef struct mystruct //Структура для хранения строк {     char *data; //Тут храним строку     int length; //Тут храним длину строки     struct mystruct *next; //Тут храним указатель на следующий элемент списка } StringListNode; //Объявляем эту структуру под псевдонимом StringListNode  int inline Contains(char *s, char c) {     int i;     for(i = 0; s[i]; i++) //Пока не встретился символ окончания строки         if(s[i] == c) //Если символы совпадают (то есть в строке есть такой символ)             return 1; //Возвращаем истину     return 0; //Если дошли до конца строки и нам ни разу не встретилась похожая буква, значит их в строке нет, возвращаем ноль }  StringListNode* AddToList(char *s, int length) {     StringListNode *p = (StringListNode*)malloc(sizeof(StringListNode)); //Выделяем память под новый элемент списка     p->length = length; //Сохраняем в этом элементе длину строки     p->data = (char*)malloc(sizeof(char)*(length+1)); //Выделяем память в элементе под строку (кол-во выделяемой памяти = кол-во символов строки + 1 (для '\0'), умноженное на место, занимаемое одной буквой     strcpy(p->data,s); //Помещаем в выделенную на предыдущем шаге область памяти строку (то есть сохраняем строку в элементе)     return p; //Возвращаем элемент }  StringListNode *StringSplit (char *s, char delims[]) {     StringListNode *head, *tail, *p; //Указатель на голову списка, хвост списка и на новый элемент     int i,j; //Счетчики     char result[255]; //Буфер     for(i = j = head = tail = 0; s[i]; i++) //Обнуляем все переменные, идем, пока s[i] != NULL, в короткой записи пока s[i]         if (Contains(delims,s[i])) //Если это разделитель то         {             if (j == 0) continue; //Если полезной информации нет (длина строки, содержащей информацию, равна нулю) пропускаем             result[j++] = '\0';  //Иначе дописываем в конец ноль             p = AddToList(result,j); //Создаем новый элемент списка, куда помещается строка и информация о её длине             if (head) //Если у нас список непустой             {                 tail->next = p; //То в конец добавляем новый элемент                 tail = tail->next; //А этот новый элемент теперь сам является последним в списке             }             else                 tail = head = p; //Иначе это единственный элемент списка, поэтому голова и хвост указывают на него             j = 0; //Строку обработали, обнулили счетчик текущей строки         }         else                     result[j++] = s[i];   //Иначе это не разделитель, а буква, значит добавляем в строку     if (j) //Если после выхода из цикла у нас осталась полезная информация (длина строки, содержащей информацию, не равна нулю), то также добавляем её в список     {         result[j++] = '\0';         tail->next = AddToList(result,j);         tail = tail->next;     }     tail->next = NULL; //После последнего элемента никаких элементов нет, поэтому обнуляем поле next хвостового элемента     return head; //Возвращаем список } 

Код максимально подробно прокомментирован. Обычно, комментирование каждой строки является плохим тоном, так как загромождает программу ненужным текстом, но мое мнение таково, что в данном случае это оправданно. Функция Contains уже имеется в том или ином виде в string.h, но когда этот код писался, я об этом еще не знал (я же тоже учусь на своих ошибках). Почему не исправлено, см.ниже.

P.S.

Кто-то может начать кидаться тухлыми помидорами и кричать, что регэкспы (регулярные выражения, выражаясь русским языком) все это разрулят за 5 секунд: да, конечно, но где тогда интерес?

P.P.S.

Не претендую на абсолютную правильность вышеописанных кодов, так как я считаю, что важна не реализация, а алгоритмическая модель данной задачи, но надеюсь, что читатель сам при необходимости сможет его исправить/дополнить. Удачи вам в ваших исследованиях!

ссылка на оригинал статьи http://habrahabr.ru/post/164915/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *