Символы и строки
Архитектура Решения
Как обычно, для демонстрации примеров данной главы построено Решение с именем главы Ch7. В Решение включены три проекта. Проект DLL с именем SearchAndSorting содержит два сервисных класса Service<T> и SortService<T>, методы которых реализуют алгоритмы поиска по образцу и сортировки массивов. Проект Windows с именем SearchAndSort имеет традиционную архитектуру с главной кнопочной формой. Два интерфейсных класса FormSearch и FormSorting обеспечивают интерфейс пользователя, позволяющий анализировать методы поиска и сортировки из DLL, подключенной к проекту. Консольный проект SymbolsAndStrings содержит класс Testing, большое число методов которого представляют собой различные тесты, иллюстрирующие работу со строками и символами. К этому проекту также подключена DLL, так что часть тестов позволяет работать с методами поиска и сортировки в консольном варианте.
Эта глава завершает вводную часть курса, изложение начал программирования. Поэтому в разделе алгоритмы и задачи основное внимание уделено методам поиска и сортировки, представляющим необходимую начальную часть образования программиста.
Классы построенной DLL являются универсальными классами с параметрами. Такие классы будут подробно рассматриваться в последующих главах курса. Возможно, не совсем корректно по отношению к читателю использовать в примерах еще не описанный инструментарий. Но, выбирая между методичностью изложения и примерами, с самого начала демонстрирующими возможности языка и стиль программирования, я склоняюсь в пользу примеров.
Алгоритмы и задачи
Так говорит история человечества. В истории компьютеров вначале было число. Долгое время вместо термина "компьютер" использовались аббревиатуры "ЭВМ" (Электронная Вычислительная Машина) и "ЦВМ" (Цифровая Вычислительная Машина), что подчеркивало цифровую сущность первых компьютеров. И использовались они тогда в отраслях, связанных с военными применениями, в зарождающейся космической отрасли, в физике - в тех областях, где господствовала цифра. Тогда в почете были физики, а не лирики с их, казалось бы, ненужными текстами.
В первых языках программирования - Фортране и Алголе практически отсутствовали средства представления текстовой информации и работы с ней. В сборнике упражнений по Алголу, подготовленном на факультете ВМК МГУ и вышедшем в 1975 году, нет ни одного упражнения по работе с текстовой информацией, все упражнения предназначены для работы с числами. Приведу еще цитату из книги, вышедшей в 1980 году и посвященной обзору расплодившихся тогда языков программирования: "Можно сказать, что для "научных" языков программирования характерно полное или почти полное отсутствие средств для работы со строками литер".
Однако время господства цифры прошло, и ей пришлось уступить символу, занявшему законное первое место в компьютерных программах. Первые задачи по обработке текстов были связаны с потребностями самого программирования. Появление алгоритмических языков стимулировало развитие теории трансляции - теоретической и практической дисциплине, занимающейся переводом текстов с одного языка на другой. Для формальных языков, каковыми являются языки программирования, задача перевода успешно решена. Для естественных языков, несмотря на некоторые успехи, впечатляющих результатов пока не получено.
Широкое применение компьютеров не только в инженерных дисциплинах, но и в бизнесе, также способствовало развитию работы с текстовыми документами.
Появление персональных компьютеров в каждом доме, а затем и появление компьютерных сетей, создало новую реальность - информационный мир. Ежедневно миллионы людей создают новые тексты, размещая их в Интернете - этом громадном хранилище текстов. Денно и нощно поисковые машины перелопачивают эту груду, индексируя их, наводя хоть какой-то порядок, позволяющий по запросу найти нужный текст. Без людей, создающих тексты, и без компьютеров, обрабатывающих эти тексты, Интернет как хранилище информации был бы бесполезным.
Здесь есть еще одна невидимая сторона дела - алгоритмическая сложность задач, решаемых в процессе поиска. Пользователям Интернета, далеким от понимания алгоритмов, может казаться совершенно естественным, что на их запрос уже через секунды выдается большое число ссылок на тексты с запрашиваемой информацией. Пользователи могут жаловаться, что ссылок слишком много, не все из них действительно соответствуют запросу, но в целом система работает удовлетворительно. У специалиста, представляющего, какие объемы текстов следует просмотреть для получения ответов, работоспособность системы должна вызывать изумление и уважение. Примитивные алгоритмы работы с текстами не смогли бы привести к успеху поиска.
Интернет - далеко не единственная область, где подобные алгоритмы играют важнейшую роль. Молекулярная биология (и ее раздел - биоинформатика) является сегодня бурно развивающейся научной областью. Как ни странно (а, может быть, вполне естественно), при анализе структур ДНК и РНК, при расшифровке генома человека работа с текстами играет определяющую роль. В книге Дэна Гансфилда "Строки" подробно рассматриваются алгоритмы работы с текстами как необходимый инструментарий решения задач вычислительной биологии. Приведу из нее некоторые цитаты, поясняющие, как биологическая информация представляется в виде текста: "Можно получить биологически осмысленные результаты, рассматривая ДНК как одномерную строку символов". Аналогичное, но более сильное предположение делается и о белках. Информация, которая лежит за биохимией, клеточной биологией, может быть представлена обычной строкой, составленной из 4-х символов G, А, Т и С. Для биологии организмов эта строка является исходной структурой данных.
Для работы с текстами на языке C# библиотека классов FCL предлагает целый набор разнообразных классов, сосредоточенных в разных пространствах имен этой библиотеки. Классы для работы с текстами находятся как в основном пространстве имен System, так и в пространствах System.Text и System.Text.RegularExpression.
Классы C#, используемые для представления строк, - char, сhar[], string, StringBuilder - связаны между собой, и из объекта одного класса нетрудно получить объект другого класса. Конструктору класса string можно передать массив символов, создав, тем самым, объект класса string. Для обратного преобразования из string в char[] следует вызвать метод ToCharArray, которым обладают объекты класса string. Достаточно вызвать метод ToString объекта StringBuilder для преобразования объекта класса StringBuilder в объект класса string. Обратное преобразование можно выполнить, передавая конструктору класса StringBuilder объект string.
Задачи
- 1. Напишите процедуру, подсчитывающую частоту использования группы символов в заданном тексте. Проведите исследование произведений двух поэтов, подсчитав частоты использования гласных и согласных, глухих и звонких согласных. Для представления текстов используйте класс char [].
- 2. Напишите процедуру, подсчитывающую частоту использования группы символов в заданном тексте. Проведите исследование произведений двух поэтов, подсчитав частоты использования гласных и согласных, глухих и звонких согласных. Для представления текстов используйте класс string.
- 3. Напишите процедуру, подсчитывающую частоту использования группы символов в заданном тексте. Проведите исследование произведений двух поэтов, подсчитав частоты использования гласных и согласных, глухих и звонких согласных. Для представления текстов используйте класс StringBuilder.
- 4. Напишите процедуру, разделяющую исходный текст на предложения. Для представления текстов используйте класс char [].
- 5. Напишите процедуру, разделяющую исходный текст на предложения. Для представления текстов используйте класс string.
- 6. Напишите процедуру, разделяющую исходный текст на предложения. Для представления текстов используйте класс StringBuilder.
- 7. Исходный текст представляет собой предложение. Напишите процедуру, разделяющую исходный текст на слова. Для представления текстов используйте класс char[].
- 8. Исходный текст представляет собой предложение. Напишите процедуру, разделяющую исходный текст на слова. Для представления текстов используйте класс string.
- 9. Исходный текст представляет собой предложение. Напишите процедуру, разделяющую исходный текст на слова. Для представления текстов используйте класс StringBuilder.
- 10. Напишите процедуру IsIder, проверяющую, является ли исходный текст правильно построенным идентификатором. Для представления текста используйте класс char [].
- 11. Напишите процедуру IsIder, проверяющую, является ли исходный текст правильно построенным идентификатором. Для представления текста используйте класс string.
- 12. Напишите процедуру IsIder, проверяющую, является ли исходный текст правильно построенным идентификатором. Для представления текста используйте класс StringBuilder.
- 13. Напишите процедуру IsInt, проверяющую, является ли исходный текст правильно построенным целым числом. Для представления текста используйте класс char [].
- 14. Напишите процедуру IsInt, проверяющую, является ли исходный текст правильно построенным целым числом. Для представления текста используйте класс string.
- 15. Напишите процедуру IsInt, проверяющую, является ли исходный текст правильно построенным целым числом. Для представления текста используйте класс StringBuilder.
- 16. Напишите процедуру IsFloat, проверяющую, является ли исходный текст правильно построенным числом с плавающей точкой. Для представления текста используйте класс char [].
- 17. Напишите процедуру IsFloat, проверяющую, является ли исходный текст правильно построенным числом с плавающей точкой. Для представления текста используйте класс string.
- 18. Напишите процедуру IsFloat, проверяющую, является ли исходный текст правильно построенным числом с плавающей точкой. Для представления текста используйте класс StringBuilder.
- 19. Напишите процедуру IsNumber, проверяющую, является ли исходный текст правильно построенным числом. Для представления текста используйте класс char [].
- 20. Напишите процедуру IsNumber, проверяющую, является ли исходный текст правильно построенным числом. Для представления текста используйте класс string.
- 21. Напишите процедуру IsNumber, проверяющую, является ли исходный текст правильно построенным числом. Для представления текста используйте класс StringBuilder.
- 22. Исходный текст представляет описание класса на C#. Напишите процедуру, выделяющую из этого текста заголовки методов класса с предшествующими им тегами summary. Для представления текстов используйте класс char [].
- 23. Исходный текст представляет описание класса на C#. Напишите процедуру, выделяющую из этого текста заголовки методов класса с предшествующими им тегами summary. Для представления текстов используйте класс string.
- 24. Исходный текст представляет описание класса на C#. Напишите процедуру, выделяющую из этого текста заголовки методов класса с предшествующими им тегами summary. Для представления текстов используйте класс StringBuilder.
- 25. Исходный текст представляет описание класса на C#. Напишите процедуру, удаляющую из этого текста теги summary и комментарии. Для представления текстов используйте класс char [].
- 26. Исходный текст представляет описание класса на C#. Напишите процедуру, удаляющую из этого текста теги summary и комментарии. Для представления текстов используйте класс string.
- 27. Исходный текст представляет описание класса на C#. Напишите процедуру, удаляющую из этого текста теги summary и комментарии. Для представления текстов используйте класс StringBuilder.
- 28. Исходный текст представляет описание класса на C#. Напишите процедуру, создающую массив строк, каждая из которых содержит описание одного из методов класса. Для представления текстов используйте класс char [].
- 29. Исходный текст представляет описание класса на C#. Напишите процедуру, создающую массив строк, каждая из которых содержит описание одного из методов класса. Для представления текстов используйте класс string.
- 30. Исходный текст представляет описание класса на C#. Напишите процедуру, создающую массив строк, каждая из которых содержит описание одного из методов класса. Для представления текстов используйте класс StringBuilder.
- 31. Исходный текст представляет описание класса на C#. Напишите процедуру, создающую массив строк, каждая из которых содержит описание одного из полей класса. Для представления текстов используйте класс char [].
- 32. Исходный текст представляет описание класса на C#. Напишите процедуру, создающую массив строк, каждая из которых содержит описание одного из полей класса. Для представления текстов используйте класс string.
- 33. Исходный текст представляет описание класса на C#. Напишите процедуру, создающую массив строк, каждая из которых содержит описание одного из полей класса. Для представления текстов используйте класс StringBuilder.
- 34. Исходный текст задает оператор языка C#. Напишите процедуру, определяющую тип оператора. Для представления текстов используйте класс char [].
- 35. Исходный текст задает оператор языка C#. Напишите процедуру, определяющую тип оператора. Для представления текстов используйте класс string.
- 36. Исходный текст задает оператор языка C#. Напишите процедуру, определяющую тип оператора. Для представления текстов используйте класс StringBuilder.
- 37. Напишите процедуру "Строгий Палиндром", определяющую, является ли заданный текст палиндромом. Напомню, палиндромом называется симметричный текст, одинаково читаемый как слева направо, так и справа налево.
- 38. Напишите процедуру "Палиндром", определяющую, является ли заданный текст палиндромом. При анализе текста:
- пробелы не учитываются;
- регистр не учитывается;
- буквы "е" и "ё", "и" и "й" считаются одинаковыми.
- 39. Напишите процедуру "Слог", разбивающую слово на слоги. Предложите свой алгоритм. За основу возьмите следующие правила:
- две подряд идущие гласные рассматриваются как одна гласная;
- число слогов определяется числом гласных букв (с учетом предыдущего правила);
- если n - число согласных между двумя соседними гласными, то n/2 согласных относятся к предыдущему слогу, а оставшиеся - к следующему. Вот примеры нескольких разбиений в соответствии с этим алгоритмом: "слог", "сло-во", "прог-ноз", "транс-крип-ция", "зоо-ма-га-зин".
Проекты
- 40. Создайте класс CharArray для представления строк и интерфейс для работы с ним. Методы класса должны включать набор методов класса string. Внутреннее представление строки должно задаваться массивом символов - char []. Методы, изменяющие размер строки, должны реализовываться функциями, как в классе string, создавая новый объект.
- 41. Создайте класс CharArray для представления строк и интерфейс для работы с ним. Методы класса должны включать набор методов класса string. Внутреннее представление строки должно задаваться массивом символов - char []. Методы, изменяющие размер строки, должны реализовываться процедурами, как в классе StringBuilder.
- 42. Создайте класс MyText для работы с текстом. Методы этого класса должны выполнять различные операции над текстом. Примеры некоторых операций даны в задачах этого раздела. Операции над текстом должны, например, позволять получать коллекции абзацев, предложений, слов текста, получать абзац, предложение, слово по его номеру, разбивать слово на слоги.
- 43. Создайте класс MyProgramText для работы с текстом программ на языке C#. Методы этого класса должны выполнять различные операции над текстом программы. Примеры некоторых операций даны в задачах этого раздела.
Поиск и Сортировка
Задачи поиска и сортировки возникают в самых разных контекстах. Рассмотрим задачу поиска в следующей постановке. Дан массив Items c элементами типа (класса) T и элемент pattern типа T, называемый образцом. Необходимо определить, встречается ли образец в массиве и, если да, определить индекс его вхождения.
Задача сортировки состоит в том, чтобы отсортировать массив Items. Предполагается, что тип T является упорядоченным типом, так что его элементы можно сравнивать. Задачу можно конкретизировать, полагая, например, что T - это тип string, и рассматривать поиск и сортировку строковых массивов. Поскольку алгоритмы поиска и сортировки практически не зависят от типа T, то отложим конкретизацию типа настолько, насколько это возможно.
Поиск
Рассмотрим три классических алгоритма поиска - линейный поиск, линейный поиск с барьером, бинарный поиск в упорядоченном массиве.
Линейный поиск
Алгоритм линейного поиска предельно ясен. В цикле по числу элементов сравнивается очередной элемент массива с образцом. При нахождении элемента, совпадающего с образцом, поиск прекращается. Если цикл завершается без нахождения совпадений, то это означает, что в массиве нет искомого элемента. Время работы такого алгоритма линейно. В худшем случае придется сравнить образец со всеми элементами, в лучшем - с одним, в среднем - число сравнений равно n/2, где n - число элементов массива. У линейного поиска есть один недостаток: если образец не присутствует в массиве, то без принятия предохранительных мер поиск может выйти за границы массива, вследствие чего может возникнуть исключительная ситуация. В классическом варианте линейного поиска приходится на каждом шаге дополнительно проверять корректность значения текущего индекса.
Чтобы эта простая задача смотрелась интереснее, рассмотрим параметризованный алгоритм с параметром T, задающим тип элементов, и его реализацию на языке C#. Построим универсальный класс (класс с родовыми параметрами):
public class Service<T> where T:IComparable<T> { }
Класс Service имеет параметр T, на который наложено ограничение - класс T должен быть наследником интерфейса IComparable, следовательно, должен реализовать метод CompareTo этого интерфейса. Содержательно это означает, что T является упорядоченным классом.
Класс Service будем рассматривать как сервисный класс, реализованный в виде модуля, предоставляющий клиентским классам некоторые сервисы, в частности, возможность осуществлять поиск в массивах любого типа. Добавим в этот класс два статических метода, реализующих алгоритм линейного поиска:
/// <summary> /// Линейный поиск образца в массиве /// </summary> /// <param name="massiv">искомый массив</param> /// <param name="pattern">образец поиска</param> /// <returns> /// индекс первого элемента, совпадающего с образцом /// или -1, если образец не встречается в массиве /// </returns> public static int SearchPattern(T[] massiv, T pattern) { for (int i = 0; i < massiv.Length; i++) if (massiv[i].CompareTo(pattern)==0) return (i); return (-1); } /// <summary> /// Вариация линейного поиска образца в массиве /// </summary> /// <param name="massiv">искомый массив</param> /// <param name="pattern">образец поиска</param> /// <returns> /// индекс первого элемента, совпадающего с образцом /// или -1, если образец не встречается в массиве /// </returns> public static int SearchPattern1(T[] massiv, T pattern) { int i = 0; while((i<massiv.Length)&& (massiv[i].CompareTo(pattern)!=0)) i++; if (i == massiv.Length) return (-1); else return (i); }
Две вариации линейного поиска отличаются лишь деталями. В первой из них проще условие цикла, но зато в тело цикла встроен оператор if, при выполнении условия которого завершается не только цикл, но и сам метод. В другой вариации усложнено условие цикла, но тело цикла совсем простое. В принципе тело цикла можно сделать пустым в этом варианте, внеся увеличение индекса во второе условие цикла. Но это уже трюк, снижающий ясность понимания программы. Трюкачество я не приветствую. Какую из эквивалентных версий выбирать - это дело программистского вкуса.
Поиск с барьером
Алгоритм линейного поиска можно упростить, избавившись от проверки дополнительного условия, если быть уверенным, что в массиве обязательно присутствует элемент, совпадающий с образцом. Иногда истинность этого условия следует из знания того, как строился массив и образец поиска. Но можно добиться выполнения этого условия принудительно, соорудив в массиве "барьер", препятствующий выходу поиска за границы массива. С этой целью массив расширяется на один элемент и в качестве последнего элемента записывается "барьер" - образец поиска. В этом случае поиск всегда найдет образец. Если образца нет среди "родных" элементов массива, то он встретится в конце в виде "барьера".
Для упрощения больше подходит вторая версия алгоритма линейного поиска. Приведу реализацию этой схемы:
/// <summary> /// Линейный поиск с барьером /// Предусловие: В массиве существует элемент, /// совпадающий с образцом pattern /// </summary> /// <param name="massiv">искомый массив</param> /// <param name="pattern">образец поиска</param> /// <returns> /// индекс первого элемента, совпадающего с образцом /// </returns> public static int SearchBarrier(T[] massiv, T pattern) { int i = 0; while (massiv[i].CompareTo(pattern) != 0) i++; return (i); }
Заметьте, сам метод никаких барьеров не строит. Он лишь формулирует предусловие, требующее существование барьерного элемента в массиве. Ответственность за выполнение предусловия лежит на клиенте. Тот, кто вызывает метод, тот и должен заботиться о выполнении предусловия. Таковы принципы проектирования по контракту. Конечно, можно построить другую реализацию, где ответственность за построение барьера берет на себя сам метод.