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

—

от автора

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

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

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

Все бы очень просто, но люди начинают пытаться воспользоваться встроенными методами, вроде 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 не будет опубликован. Обязательные поля помечены *