Символы и строки
Бинарный поиск
У этого метода поиска много синонимичных названий - метод деления пополам, двоичный или бинарный поиск, метод дихотомии. Все эти названия отражают тот приятный факт, что в заранее отсортированном массиве сравнение с одним элементом позволяет вдвое уменьшить число кандидатов. Для этого достаточно сравнить образец с элементом массива, стоящим в середине. Если образец совпадает с этим элементом, то элемент найден и поиск завершается. Если образец меньше срединного элемента, то размеры области поиска сокращаются вдвое: элемент может находиться лишь в первой половине массива. Если образец больше срединного элемента, он находится во второй половине массива. Введение двух параметров - start и finish, задающих границы области поиска, позволяет достаточно просто описать схему алгоритма. Алгоритм бинарного поиска намного эффективнее линейного поиска в особенности для больших массивов. Нетрудно понять, что для отсортированного массива из n элементов он требует не более чем сравнений образца с элементами массива. Вот его возможная реализация:
/// <summary> /// Бинарный поиск образца в упорядоченном массиве /// Предусловие: Массив упорядочен /// </summary> /// <param name="massiv">искомый массив</param> /// <param name="pattern">образец поиска</param> /// <returns> /// индекс элемента, совпадающего с образцом, /// но не обязательно индекс первого вхождения, /// -1, если образец не встречается в массиве /// </returns> public static int BinSearch(T[] massiv, T pattern) { int start = 0, finish = massiv.Length-1, mid = (start+finish)/2; while (start <= finish) { if (massiv[mid].CompareTo(pattern) == 0) return (mid); if(massiv[mid].CompareTo(pattern) == 1) finish = mid-1; else start = mid+1; mid = (start+finish)/2; } return(-1); }
Как клиентский класс может пользоваться сервисами универсального класса Service? Приведу примеры работы с методами класса, когда в качестве клиента выступает класс из консольного проекта и класс из Windows проекта. Начнем с консоли. Клиент работает с массивом строк класса string. Он предпочитает использовать метод поиска с барьером и сам заботится об организации барьера до вызова метода:
public void TestSearch() { string answer = "yes"; int n; Console.WriteLine("Введите n - число элементов массива"); n = Convert.ToInt32(Console.ReadLine()); string[] ar1 = new string[n + 1]; for (int i = 0; i < n; i++) { Console.WriteLine("Введите строку - элемент" + " массива с номером {0}", i); ar1[i] = Console.ReadLine(); } do { string pat1; Console.WriteLine("Введите строку - образец поиска"); pat1 = Console.ReadLine(); ar1[n] = pat1; //Выполнено условие метода поиска с барьером int k = Service<string>.SearchBarrier(ar1, pat1); if (k != n) Console.WriteLine("Образец pat1 = {0} найден в массиве!" + "\nЭто элемент ar[{1}] = {2} ", pat1, k, ar1[k]); else Console.WriteLine("Образец pat1 ={0} не найден!", pat1); Console.WriteLine("Продолжим? (yes/no"); answer = Console.ReadLine(); } while (answer != "no"); }
На рис. 7.8 показаны результаты работы метода.
Метод TestSearch включен в класс Testing консольного проекта SymbolsAndStrings, многократно использованного в предыдущих примерах. К консольному проекту присоединена библиотека классов - DLL с именем, содержащая универсальный класс Service<T>. При вызове метода этого класса, реализующего поиск с барьером, задается параметр типа, характеризующий тип элементов массива, в котором ведется поиск:
Service<string>.SearchBarrier(ar1, pat1)
Для проведения более полных экспериментов с методами поиска и проведения сравнения времени работы методов поиска построен Windows-проект с именем SearchAndSort, к которому подключена та же библиотека классов SearchAndSorting. Архитектурно новый проект представляет проект с главной кнопочной формой, которая позволяет перейти к анализу методов поиска либо к анализу методов сортировки, о которых пойдет речь далее. На рис. 7.9 показана форма, которая обеспечивает интерфейс, необходимый при исследовании поведения методов поиска по образцу.
На рисунке можно видеть, что интерфейс позволяет пользователю создать массив нужной размерности, заполнить его случайными числами из заданного диапазона, отсортировать массив, показать его текущее состояние. В проекте контролируется корректность задания исходных данных. Главное, что позволяет интерфейс, - анализировать, насколько успешно справляются различные методы с поиском заданного образца. Интерфейс позволяет также оценить время, затрачиваемое каждым из методов на поиск фиксированного образца в одном и том же массиве. На рисунке показано время, измеренное в одном из экспериментов. Как и следовало ожидать, поиск с барьером дает лучшие по времени результаты, чем классический линейный поиск. Поскольку эксперимент велся на отсортированном массиве, где применим и бинарный поиск, то можно видеть, что последний дает на порядок лучший результат.
Приводить полностью код интерфейсного класса не буду, предоставляя читателям восстановить его. Ограничусь лишь приведением некоторых фрагментов кода. Вот код обработчика события Click командной кнопки, ответственной за вызов метода поиска с барьером:
private void buttonBarierSearch_Click(object sender, EventArgs e) { int n = arr.Length - 1; arr[n] = pattern; //барьер int result; result = Service<int>.SearchBarrier(arr, pattern); if (result != n) textBoxResult.Text = PATTERN_IS_FOUND + result; else textBoxResult.Text = PATTERN_IS_NOT_FOUND; }
Нетрудно видеть, что в сравнении с консольным проектом единственное изменение при вызове метода поиска из класса Service<T> состоит в том, что передается другое значение параметра типа, поскольку поиск ведется в массиве целых чисел.
Рассмотрим теперь подробнее, как оценивается время, затрачиваемое различными методами на поиск образца в массиве. Для этой цели используется стандартный прием, неоднократно используемый в примерах этого курса. В классе Service<T> определяется функциональный тип:
public delegate int SearchMethod(T[] arr, T pattern);
Все рассматриваемые нами методы поиска являются экземплярами этого типа, поскольку их сигнатуры совпадают с сигнатурой, заданной делегатом SearchMethod. В класс Service<T> добавлен следующий метод:
public static long HowLong(SearchMethod search, int count, T[] arr, T pattern) { DateTime start, finish; start = DateTime.Now; for(int i =0; i < count; i++) search(arr,pattern); finish = DateTime.Now; return finish.Ticks - start.Ticks; }
Метод HowLong позволяет оценить время работы метода, переданного в качестве первого аргумента. Это может быть любой метод, принадлежащий типу SearchMethod. Время здесь измеряется в тиках. Напомню, что один тик равен 100 наносекундам или 0.0001 миллисекунды.
Рассмотрим теперь, как вызывается метод HowLong в обработчике события соответствующей командной кнопки интерфейсного класса:
private void buttonBinSearchTime_Click(object sender, EventArgs e) { textBoxTimeBinSearch.Text = Service<int>.HowLong (Service<int>.BinSearch, count, arr, pattern).ToString(); }
Задачи
- 44. Создайте DLL, включающую сервисный класс с тремя методами поиска по образцу в массивах типа double. Постройте консольный и Windows-проекты, использующие эти методы поиска. Получите оценки времени работы методов поиска.
- 45. Создайте DLL, включающую сервисный класс с тремя методами поиска по образцу в массивах типа StringBuilder. Постройте консольный и Windows-проекты, использующие эти методы поиска. Получите оценки времени работы методов поиска.
- 46. Создайте DLL, включающую сервисный класс с тремя методами поиска по образцу в массивах типа int. Постройте консольный и Windows-проекты, использующие эти методы поиска. Получите оценки времени работы методов поиска.
- 47. Создайте DLL, включающую сервисный класс с тремя методами поиска по образцу в массивах типа Person. Класс Person определите самостоятельно. Реализуйте возможность поиска по различным полям объекта (по имени, возрасту, адресу). Постройте Windows-проект, использующий эти методы поиска.
- 48. Создайте DLL, включающую сервисный класс с тремя методами поиска по образцу в массивах типа Point. Класс Point определите самостоятельно. Реализуйте возможность поиска по различным полям объекта (по декартовым координатам точки, по полярным координатам). Постройте Windows-проект, использующий эти методы поиска.
- 49. Создайте DLL, включающую сервисный класс с тремя методами поиска по образцу в массивах типа Account. Класс Account определите самостоятельно. Реализуйте возможность поиска по различным полям объекта (по номеру банковского счета, по размеру вклада). Постройте Windows-проект, использующий эти методы поиска.
- 50. На основе приведенного описания класса Service<T> создайте собственный универсальный класс, включающий различные варианты метода поиска. Создайте Windows-интерфейс для работы с этим классом по образцу интерфейса, приведенного на рис. 7.9.
Сортировка
Задача сортировки формулируется достаточно просто. Дан массив Ar с элементами типа T. Тип (класс) T является упорядоченным типом, так что для него определена операция сравнения элементов. Отсортировать массив можно по возрастанию или по убыванию. В первом случае для всех элементов массива выполняется условие , во втором - справедливо условие . Порядок сортировки можно задавать как параметр метода, что сказывается лишь на операции сравнения элементов - "больше" или "меньше".
Методов сортировки великое множество. Классическим трудом является третий том "Искусства программирования" Д. Кнута [Кнут], который так и называется "Сортировки". Одним из основных критериев классификации методов сортировки является сложность метода сортировки - временная и емкостная - T(n) и P(n). В первом случае нас интересует время сортировки произвольного массива из n элементов, во втором - дополнительная память, требуемая в процессе сортировки. Говоря о времени сортировки, можно рассматривать минимальное, максимальное или среднее время сортировки. Время сортировки определяется числом требуемых операций, которые, в свою очередь, разделяются на операции сравнения элементов и операции обмена элементами, когда два элемента Ar[i] и Ar[j] обмениваются местами.
Методы сортировки за время порядка O(n2)
В ситуациях, когда приходится сортировать массивы небольшой размерности, разумно пользоваться простыми методами сортировки. Простые и естественные способы сортировки требуют, как правило, времени работы порядка . Эти методы сортируют массивы небольшой размерности быстрее, чем их соперники - более эффективные по порядку, но и более сложные методы сортировки. Но понятно, что у каждого из квадратичных методов сортировки есть свой предел - то максимальное значение n, после которого эффективные методы со сложностью начинают работать быстрее.
Рассмотрим алгоритмы сортировки с квадратичной сложностью, начиная с простейших, интуитивно понятных.
Сортировка SortMin (SortMax)
Две сортировки минимумами и максимумами являются вариации алгоритма, называемого часто "простым выбором". Идея алгоритма прозрачна и состоит в том, чтобы найти минимальный (максимальный) элемент массива и поставить его на первое (последнее) место. Затем применить тот же прием к массиву без первого (последнего) элемента, повторяя эту схему, пока оставшаяся часть массива не будет состоять из одного элемента.
Сортировка SortMinMax
Эта сортировка является слегка улучшенным вариантом предыдущей сортировки, когда минимальный и максимальный элементы находятся одновременно. Они и меняются местами с первым и соответственно последним элементами текущей части массива. Повышение эффективности достигается за счет того, что одновременный поиск максимума и минимума можно выполнить быстрее, чем при раздельном их поиске.
Сортировка SortBubble (SortBall)
Эти две вариации одного алгоритма сортировки относят к классу "обменных сортировок". В каждом алгоритме сортировки присутствуют операции сравнения элементов и обмена элементов. Но алгоритмы могут отличаться тем, какие операции превалируют в реализации алгоритма. В сортировках прямого выбора минимумами и максимумами обмен выполняется только после того, как сделан выбор нужного элемента, требующий многократных проверок. В обменных сортировках обмен элементов является основной операцией в процессе сортировки. И те и другие методы имеют свои достоинства и, соответственно, недостатки. Операции обмена обычно более дорогие (требуют больше времени), чем операции сравнения. В этом преимущество методов прямого выбора. Но в обменных сортировках за один проход не только один элемент становится на свое место, но и другие элементы стремятся занять свои места, что позволяет ускорить сортировку.
Идея алгоритма пузырьковой сортировки SortBubble, принадлежащей классу обменных сортировок, состоит в том, чтобы, начиная с конца массива, сравнивать два соседних элемента и, если нарушается упорядоченность, производить обмен элементами - более легкий элемент меняется местами со своим соседом. Очевидно, что при первом проходе массива минимальный элемент, как самый легкий всплывет наверх, подобно пузырьку воздуха, и станет на первое место. Важно то, что при этом будут всплывать, приближаясь к своим законным местам, и другие легкие элементы. Обменные сортировки хорошо работают на почти упорядоченных массивах. Достоинство алгоритма еще и в том, что он позволяет собрать важную информацию - на каждом проходе можно подсчитывать число обменов. Если оно равно 0, то массив уже упорядочен, и сортировку можно прекращать.
Алгоритм "тяжелого шарика" SortBall является симметричной вариацией пузырьковой сортировки. Работа начинается с начала массива, и в процессе обмена вниз опускаются тяжелые элементы, так что на первом проходе максимальный элемент станет на последнее место.