УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 1 из 62 [Введите текст] [Введите текст] МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РЕСПУБЛИКИ КАЗАХСТАН ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ имени ШАКАРИМА г. СЕМЕЙ Документ СМК 3 уровня Учебно-методическое пособие “Методы поиска” УМК Редакция №1 от 28.08.2013 г. сортировок УМКД 042-14-0203.1.20.47/03 и УЧЕБНО-МЕТОДИЧЕСКИЙ КОМПЛЕКС ДИСЦИПЛИНЫ «Системное программное обеспечение» для специальности 5В070200 «Автоматизация и управление» Учебно-методическое пособие Семей 2013 УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 2 из 62 [Введите текст] СОДЕРЖАНИЕ 1 Лекции 3 2 Практические занятия 34 УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 3 из 62 [Введите текст] Лекции Лекция № 1 Методы сортировок. Введение. Методы поиска в основной памяти В этой части книги будут обсуждаться структуры данных в основной памяти и методы их использования, предназначенные для поиска данных в соответствии со значениями их ключей. Эта задача является не менее распространенной в программировании, чем внутренняя сортировка данных. Главным образом, распространены два подхода - поиск в динамических древовидных структурах и поиск в таблицах на основе хэширования. Рассмотрению разновидностей этих подходов и посвящены следующие два раздела. Заметим, что мы намеренно выделили в отдельную часть структуры данных и методы поиска данных во внешней памяти. Несмотря на использование того же набора терминов (деревья и хэширование) поиск во внешней памяти и критерии оценки эффективности алгоритмов существенно отличаются от тех, которые применимы в случае расположения данных в основной памяти. Методы поиска в основной памяти на основе деревьев Понятия, связанные с деревьями, широко известны и интуитивно понятны. Тем не менее, для однозначного понимания содержимого этого раздела (и соответствующего раздела следующей части курса) мы приведем несколько не слишком формальных определений и примеров. Существует несколько возможных определений дерева. Например, с точки зрения теории графов деревом часто называют неориентированный граф с выделенной вершиной (корнем), который не содержит циклов. Нас будут интересовать не произвольные графы, а только ориентированные деревья, причем с точки зрения программистов. Поэтому мы используем следующее рекурсивное определение: дерево R с базовым типом T - это либо (a) пустое дерево (не содержащее ни одной вершины), либо (b) некоторая вершина типа T (корень дерева) с конечным (возможно, нулевым) числом связанных с ней деревьев с базовым типом T (эти деревья называются поддеревьями дерева R). Из этого определения, в частности, следует, что однонаправленный список (рисунок 4.1) является деревом. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 4 из 62 [Введите текст] Рис.1. Деревья можно представлять по-разному (это всего лишь однородная иерархическая структура). Например, на рисунках 4.1 и 4.2 показаны два разных способа представления одного и того же дерева, у которого базовый тип содержит множество букв латинского алфавита. Сразу заметим, что графовое представление на рисунке 4.2 больше соответствует специфике программирования. Рис.2. Придерживаясь естественной для графового представления терминологии, мы будем называть связи между поддеревьями ветвями, а корень каждого поддерева - вершиной. Упорядоченным деревом называется такое, у которого ветви, исходящие из каждой вершины, упорядочены. Например, два упорядоченных дерева на рисунке 4.3 различаются. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 5 из 62 [Введите текст] [Введите текст] Рис.3. По определению, корень дерева находится на уровне 0, а все вершины дерева, непосредственно связанные с вершиной уровня i, находятся на уровне i+1. Вершина x уровня i, непосредственно связанная с вершиной y уровня i+1, называется непосредственным предком (или родителем) вершины y. Такая вершина y соответственно называется непосредственным потомком (или сыном) вершины x. Вершина без непосредственных потомков называется листовой (или терминальной), нелистовые вершины называются внутренними. Под степенью внутренней вершины понимается число ее непосредственных потомков. Если все вершины имеют одну и ту же степень, то она полагается степенью дерева. На самом деле, всегда можно добиться того, чтобы любая вершина дерева имела одну и ту же степень путем добавления специальных вершин в тех точках, где отсутствуют поддеревья (рисунок 4.4). Число вершин (или ветвей), которые нужно пройти от корня к вершине x, называется длиной пути к вершине x. Высотой (или глубиной) дерева будем называть максимальную длину его вершины. Лекция № 2 Сортировка с помощью прямого включения Для организации поиска в основной памяти особое значение имеют упорядоченные двоичные (бинарные) деревья (как, например, на рисунке 4.3). В каждом таком дереве естественно определяются левое и правое УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 6 из 62 [Введите текст] поддеревья. Двоичное дерево называется идеально сбалансированным, если число вершин в его левом и правом поддеревьях отличается не более, чем на 1 (легко видеть, что при соблюдении этого условия длины пути до любой листовой вершины дерева отличаются не больше, чем на 1). Примеры идеально сбалансированных деревьев показаны на рисунке 4.5. Рис. 5. Двоичные деревья обычно представляются как динамические структуры с базовым типом записи T, в число полей которого входят два указателя на переменные типа T. При использовании в целях поиска элементов данных по значению уникального ключа применяются двоичные деревья поиска, обладающие тем свойством, что для любой вершины дерева значение ее ключа больше значения ключа любой вершины ее левого поддерева и больше значения ключа любой вершины правого поддерева (рисунок 4.6). Для поиска заданного ключа в дереве поиска достаточно пройти по одному пути от корня до (возможно, листовой) вершины (рисунок 4.7). Высота идеально сбалансированного двоичного дерева с n вершинами составляет не более, чем log n (логарифм двоичный), поэтому при применении таких деревьев в качестве деревьев поиска (рисунок 4.8) потребуется не более log n сравнений. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 7 из 62 [Введите текст] [Введите текст] Рис. 6. Путь поиска ключа по значению “23” Рис. 7. Идеально сбалансированное двоичное дерево Применение деревьев как объектов с динамической структурой особенно полезно, если допускать выполнение не только операции поиска по значению ключа, но и операций включения новых и исключения существующих ключей. Если не принимать во внимание потенциальное желание поддерживать идеальную балансировку дерева, то процедуры включения и исключения ключей очень просты. Для включения в дерево вершины с новым ключом x по общим правилам поиска ищется листовая вершина, в которой находился бы этот ключ, если бы он входил в дерево. Возможны две ситуации: (a) такая вершина не существует; (b) вершина существует и уже занята, т.е. содержит некоторый ключ y. В первой ситуации создается недостающая вершина, и в нее заносится значение ключа x. Во второй УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 8 из 62 [Введите текст] ситуации после включения ключа x эта вершина в любом случае становится внутренней, причем если x > y, то ключ x заносится в новую листовую вершину - правого сына y, а если x < y - то в левую. Четыре потенциально возможных случая проиллюстрированы на рисунке 4.9. (a) (b) УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (c) (d) Страница 9 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 10 из 62 [Введите текст] (e) Рис. 8. При выполнении исключения ключа из дерева также прежде всего выполняется поиск ключа. Если ключ обнаруживается, то возможны следующие случаи: (a) ключ содержится в листовой вершине, у вершиныотца которой имеются два сына; (b) ключ содержится в листовой вершине, являющей единственным сыном своего отца; (c) ключ содержится во внутренней вершине, имеющей только левого или только правого сына; (d) ключ содержится во внутренней вершине, имеющей и левого, и правого сыновей. В случае (a) соответствующая листовая вершина ликвидируется, а у ее отца остается только один сын. В случае (b) листовая вершина ликвидируется, а ее отец становится новой листовой вершиной. В случае (c) внутренняя вершина ликвидируется, и ее место занимает единственный сын (он может быть внутренней или листовой вершиной. В случае (d) внутренняя вершина ликвидируется, и заменяется на листовую или внутреннюю вершину, достигаемую по самому правому пути от левого сына внутренней вершины. Эта вершина наследует левого и правого сыновей ликвидируемой вершины. Возможные варианты иллюстрируются на рисунке 4.10. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (a) (b) Страница 11 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (c) (d) Страница 12 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 13 из 62 [Введите текст] [Введите текст] (e) (f) Рис. 9. Исключение ключа из двоичного дерева Поддержка дерева поиска в идеально сбалансированном состоянии требует существенного усложнения (с соответствующим увеличением накладных расходов) операций включения и исключения ключей. Кроме того, как показано в книге Вирта, при равномерном распределении значений УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 14 из 62 [Введите текст] включаемых и исключаемых ключей использование идеально сбалансированных деревьев поиска дает выигрыш не более 30% (имеется в виду число сравнений, требующихся при поиске). Поэтому на практике идеально сбалансированные деревья поиска используются крайне редко. Лекция № 3 Сортировка с помощью прямого выбора Как видно из содержимого предыдущего подраздела, идеально сбалансированные деревья представляют, в большей степени, чисто теоретический интерес, поскольку поддержание идеальной сбалансированности требует слишком больших накладных расходов. В 1962 г. советские математики Адельсон-Вельский и Ландис предложили менее строгое определение сбалансированности деревьев, которое в достаточной степени обеспечивает возможности использования сбалансированных деревьев при существенно меньших расходах на поддержание сбалансированности. Такие деревья принято называть АВЛ-деревьями (в соответствии с именами их первооткрывателей). По определению, двоичное дерево называется сбалансированным (или АВЛ) деревом в том и только в том случае, когда высоты двух поддеревьев каждой из вершин дерева отличаются не более, чем на единицу. При использовании деревьев, соответствующих этому определению, обеспечивается простая процедура балансировки при том, что средняя длина поиска составляет O(log n), т.е. практически не отличается от длины поиска в идеально сбалансированных деревьях. Как доказали Адельсон-Вельский и Ландис, АВЛ-дерево никогда не превышает по глубине аналогичное сбалансированное дерево больше, чем на 45%. Чтобы понять, как устроены "самые плохие" АВЛ-деревья, попробуем построить сбалансированное дерево с высотой h, содержащее минимальное число вершин. Обозначим такое дерево через Th. Понятно, что T0 - это пустое дерево, а T1 - дерево с одной вершиной. Дерево Th строится путем добавления к корню двух поддеревьев типа T(h-1). Одно из таких поддеревьев должно иметь высоту h-1, а другое может иметь глубину h-2. Такие "плохо" сбалансированные деревья называются деревьями Фибоначчи (поскольку принцип их построения напоминает принцип построения чисел Фибоначчи) и определяются рекурсивно следующим образом: (a) пустое дерево есть дерево Фибоначчи высоты 0; (b) единственная вершина есть дерево Фибоначчи высоты 1; (c) если T(h-1) и T(h-2) являются деревьями Фибоначчи высотой h-1 и h-2 соответственно, а x - новый корень дерева, то Th = <T(h-1), x, T(h2)> есть дерево Фибоначчи высотой h; УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 15 из 62 [Введите текст] (d) другие деревья Фибоначчи не существуют. Примеры деревьев Фибоначчи высотой 2, 3 и 4 показаны на рисунке 4.11. (а) Дерево Фибоначчи высотой 2 (b) Дерево Фибоначчи высотой 3 ( c ) Дерево Фибоначчи высотой 4 Рис. 10. Примеры деревьев Фибоначчи Число вершин в дереве Th определяется из следующего рекуррентного соотношения: N0 = 0; N1 = 1; Nh = N(h-1) +1 + N(h-2) УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 16 из 62 [Введите текст] Эти числа определяют число вершин в АВЛ-дереве в худшем случае и называются "числами Леонарда". Заметим, что из этого соотношения следует, что длина пути от корня любого листа в АВЛ-дереве может отличаться не более, чем на единицу. Рассмотрим, как можно поддерживать балансировку АВЛ-дерева при выполнении операций включения и исключения ключей. Начнем с операции включения. Пусть рассматриваемое дерево состоит из корневой вершины r и левого (L) и правого (R) поддеревьев. Будем обозначать через hl высоту L, а через hr - высоту R. Для определенности будем считать, что новый ключ включается в поддерево L. Если высота L не изменяется, то не изменяются и соотношения между высотой L и R, и свойства АВЛ-дерева сохраняются. Если же при включении в поддерево L высота этого поддерева увеличивается на 1, то возможны следующие три случая: (a) если hl = hr, то после добавления вершины L и R станут разной высоты, но свойство сбалансированности сохранится; (b) если hl < hr, то после добавления новой вершины L и R станут равной высоты, т.е. сбалансированность общего дерева даже улучшится; (c) если hl > hr, то после включения ключа критерий сбалансированности нарушится, и потребуется перестройка дерева. Имеет смысл рассмотреть две разные ситуации. В первой ситуации новая вершина добавляется к левому поддереву L, во второй - к правому поддереву. Правила восстановления балансировки показаны на рисунке 4.12 и проиллюстрированы примерами на рисунке 4.13. (a) УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (b) (c) (d) Рис. 11. Страница 17 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (a) (b) (c) Страница 18 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 19 из 62 [Введите текст] (d) Рис. 12. Как кажется, в данном случае рисунки лучше проясняют суть явления, чем текст и тем более компьютерная программа. Действия, которые требуются для балансировки, авторы механизма назвали "поворотами". Действительно, если внимательно посмотреть на то, что происходит с деревом, это действительно напоминает его повороты относительно выбранной вершины. При исключении вершин из АВЛ-дерева также возможна его не слишком сложная балансировка. Мы не будем приводить описание требующихся процедур, а проиллюстрируем их на нескольких последовательных примерах (рисунок 4.14). (a) УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (b) (c) (d) Страница 20 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (e) (f) (g) Рис. 13. Страница 21 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 22 из 62 [Введите текст] Известно, что оценкой стоимости поиска в АВЛ-дереве, а также выполнения операций включения и исключения ключей является O(log n), т.е. эти деревья при поиске ведут себя почти так же хорошо, как и идеально сбалансированные деревья, а поддержка балансировки при включениях и исключениях обходится гораздо дешевле. Лекция № 4 Сортировка с помощью прямого обмена (метод пузырька) Пусть дерево поиска содержит n вершин, и обозначим через pi вероятность обращения к i-той вершине, содержащей ключ ki. Сумма всех pi, естественно, равна 1. Постараемся теперь организовать дерево поиска таким образом, чтобы обеспечить минимальность общего числа шагов поиска, подсчитанного для достаточно большого количества обращений. Будем считать, что корень дерева имеет высоту 1 (а не 0, как раньше), и определим взвешенную длину пути дерева как сумму pi*hi (1<=i<=n), где hi - длина пути от корня до i-той вершины. Требуется построить дерево поиска с минимальной взвешенной длиной пути. В качестве примера рассмотрим возможности построения дерева поиска для трех ключей 1, 2, 3 с вероятностями обращения к ним 1/7, 2/7 и 4/7 соответственно (рисунок 4.15). Посчитаем взвешенную длину пути для каждого случая. В случае (a) взвешенная длина пути P(a) = 1*4/7 + 2*2/7 + 3*1/7 = 11/7. Аналогичные подсчеты дают результаты P(b)=12/7; P(c)=12/7; P(d)=15/7; P(e)=17/7. Следовательно, оптимальным в интересующем нас смысле оказалось не идеально сбалансированное дерево (c), а вырожденное дерево (a). На практике приходится решать несколько более общую задачу, а именно, при построении дерева учитывать вероятности неудачного поиска, т.е. поиска ключа, не включенного в дерево. В частности, при реализации сканера желательно уметь эффективно распознавать идентификаторы, которые не являются ключевыми словами. Можно считать, что поиск по ключу, отсутствующему в дереве, приводит к обращению к "специальной" вершине, включенной между реальными вершинами с меньшим и большим значениями ключа соответственно. Если известна вероятность qj обращения к специальной j-той вершине, то к общей средней взвешенной длине пути дерева необходимо добавить сумму qj*ej для всех специальных вершин, где ej - высота j-той специальной вершины. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] (a) (b) (c) (d) (e) Рис. 14. Страница 23 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 24 из 62 [Введите текст] При построении дерева оптимального поиска вместо значений pi и qj обычно используют полученные статистически значения числа обращений к соответствующим вершинам. Процедура построения дерева оптимального поиска достаточно сложна и опирается на тот факт, что любое поддерево дерева оптимального поиска также обладает свойством оптимальности. Поэтому известный алгоритм строит дерево "снизу-вверх", т.е. от листьев к корню. Сложность этого алгоритма и расходы по памяти составляют O(n2). Имеется эвристический алгоритм, дающий дерево, близкое к оптимальному, со сложностью O(n*log n) и расходами памяти - O(n). Деревья цифрового поиска Методы цифрового поиска достаточно громоздки и плохо иллюстрируются. Поэтому мы кратко остановимся на наиболее простом механизме - бинарном дереве цифрового поиска. Как и в деревьях, обсуждавшихся в предыдущих разделах, в каждой вершине такого дерева хранится полный ключ, но переход по левой или правой ветви происходит не путем сравнения ключааргумента со значением ключа, хранящегося в вершине, а на основе значения очередного бита аргумента. Поиск начинается от корня дерева. Если содержащийся в корневой вершине ключ не совпадает с аргументом поиска, то анализируется самый левый бит аргумента. Если он равен 0, происходит переход по левой ветви, если 1 - по правой. Если не обнаруживается совпадение ключа с аргументом поиска, то анализируется следующий бит аргумента и т.д., пока либо не будут проверены все биты аргумента, или мы не наткнемся на вершину с отсутствующей левой или правой ссылкой. На рисунке 4.16 показан пример дерева цифрового поиска для некоторых заглавных букв латинского алфавита. Считается, что буквы кодируются в соответствии с кодовым набором ASCII, а для их представления и поиска используются 5 младших бит кода. Например, код буквы A равен 41(16), а представляться A будет как последовательность бит 00001. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 25 из 62 [Введите текст] Лекция № 5 Шейкерная сортировка Замечание по поводу терминологии. В течение многих лет в сообществе российских программистов обсуждался вопрос применения русскоязычной терминологии, относящейся к хэшированию. Кроме упомянутого выше термина "расстановка", предлагалось использовать "русский" термин "рандомизация". Поскольку, как видно, для этого подхода нет подходящего названия на русском языке, мы будем использовать транслитерацию английского термина "hashing" - хэширование. Совершенное хэширование Вырожденный пример абсолютно совершенной хэш-функции приведен на рисунке 4.17, где ключами являются коды заглавных букв латинского алфавита (в кодировке ASCII), а хэш-функция отрезает от кода буквы младшие пять бит. Рис.15. В более сложных случаях для построения совершенной хэш-функции используются специальные программы (основанные на эмпирических алгоритмах), которые по заданному набору ключей генерируют функции УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 26 из 62 [Введите текст] отображения ключей в индексы массивов хранимых записей. Вообще говоря, выработанная совершенная хэш-функция не гарантирует, что все элементы массива будут заполнены записями, соответствующими значениям ключей. Более того, при добавлении хотя бы одного нового ключа к ранее заданному набору потребуется генерация новой хэш-функции и соответствующая ей перестановка элементов массива записей. Прием совершенного хэширования дает превосходные результаты при задании известного заранее не очень большого набора ключей, но не помогает в случае необходимости динамического включения или исключения записей. Коллизии при хэшировании и способы их разрешения Хэширование может обеспечить хорошие результаты и в том случае, когда набор ключей заранее неизвестен. В этом случае не приходится говорить о генерации совершенной хэш-функции. Даже если известен диапазон значений ключей, то в лучшем случае применение совершенного хэширования потребует наличия в памяти массива с размером, равным мощности множества ключей. Кроме того, неизвестны выполняемые в разумное время алгоритмы генерации совершенной хэш-функции для множества ключей большой мощности. Распространенным методом является использование эмпирически подобранной хэш-функции, которая (a) по значению ключа производит значение индекса в границах массива и (b) равномерно распределяет ключи по элементам массива. Если ORD(k) обозначает порядковый номер ключа k в упорядоченном множестве допустимых ключей, а N - число элементов в массиве записей, то одной из наиболее естественных хэш-функций является H(k) = ORD(k) MOD N, т.е. взятие остатка от деления порядкового номера ключа на число элементов массива. Такая функция выполняется очень быстро, если N является степенью числа 2. Для числовых ключей функция обеспечивает достаточную равномерность распределения ключей в массиве. Однако если ключом является последовательность символов (что чаще всего и бывает), то при применении такой функции возникает большая вероятность выработки одного и того же значения для ключей, отличающихся небольшим числом символов. Ситуацию несколько облегчает использование в качестве N простого числа. Вычисление функции становится более сложным, но вероятность выработки одного значения для разных ключей уменьшается. Используются и более сложные способы вычисления хэш-функции основанные, например, на вырезке поднабора бит из битового представления УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 27 из 62 [Введите текст] ключа или вычислении квадратичного выражения от ORD(k). Но в любом случае с ненулевой вероятностью хэш-функция может выдать одно значение для разных значений ключа. Такая ситуация называется коллизией ключей и проявляется в том, что при попытке занести в хэш-таблицу запись с новым ключом мы наталкиваемся на то, что требуемый элемент массива уже занят. Одним из выходов (к которому рано или поздно, может быть, придется прибегнуть) является увеличение размера массива, образование новой хэшфункции и расстановка заново всех занятых элементов массива. Но поскольку возникновение коллизии может являться флуктуацией, до поры до времени обычно пытаются разрешать эту ситуацию другими способами. Традиционно принято различать методы прямой адресации (ключ, появление которого вызвало коллизию, помещается в один из свободных элементов хэш-таблицы) и методы цепочек (записи, для ключей которых выработано одинаковое значение хэш-функции связываются в линейный список). Лекция № 6 Сортировка Шелла Для обеспечения равномерного распределения ключей в хэш-таблице при наличии коллизий можно применять метод двойного хэширования. Он состоит в том, что если при включении или поиска ключа в хэш-таблице возникает коллизия, то к ключу (или к комбинации ключа и текущего индекса массива) применяется вторичная хэш-функция, значение которой указывает циклическое смещение в массиве от текущего индекса к элементу, в который следует включать или в котором следует искать требуемую запись (рисунки 4.21, 4.22). Рис. 16. Поскольку метод двойного хэширования в полной форме приводит к слишком большим накладным расходам, на практике часто используется упрощенная форма этого метода, не гарантирующая равномерности записей УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 28 из 62 [Введите текст] в массиве, но обеспечивающая лучшие результаты, чем линейное зондирование. Один из способов состоит в использовании квадратичной функции при вычислении индекса следующей пробы. В этом случае последовательность вычисления индексов проб при включении или поиске записи является следующей: h0 = H(K) ........ hi = (h0 + i^2) MOD N (i>0) Основным недостатком метода квадратичных проб является то, что для включаемой записи может не найтись свободного элемента массива даже в том случае, когда реально около половины элементов являются свободными. Рис. 17. Использование цепочек переполнения Это один из наиболее очевидных способов разрешения коллизий. Если по смыслу в элементах массива хранятся записи типа T, то в данном случае к записи добавляется поле типа ссылки на T. При возникновении коллизии по причине включения новой записи для нового элемента выделяется динамическая память, и он включается в конец линейного списка, который начинается от первичного элемента массива. Если коллизия возникает при поиске ключа, то список переполнения просматривается либо до момента нахождения требуемого ключа, либо до конца, что означает отсутствие искомой записи (рисунок 4.23). УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 29 из 62 [Введите текст] [Введите текст] Рис.18. Метод цепочек переполнения легко реализуется, понятен, но потенциально приводит к излишним расходам памяти. Лекция № 7 Сортировка с разделением (быстрая сортировка) Базовым "древовидным" аппаратом для поиска данных во внешней памяти являются B-деревья. В основе этого механизма лежат следующие идеи. Вопервых, поскольку речь идет о структурах данных во внешней памяти, общее время доступа к которой определяется в основном не объемом последовательно расположенных данных, а временем подвода магнитных головок (см. введение в Часть 3), то выгодно получать за одно обращение к внешней памяти как можно больше информации, учитывая при этом необходимость экономного использования основной памяти. При сложившемся подходе к организации основной памяти в виде набора страниц равного размера естественно считать именно страницу единицей обмена с внешней памятью. Во-вторых, желательно обеспечить такую поисковую структуру во внешней памяти, при использовании которой поиск информации по любому ключу требует заранее известного числа обменов с внешней памятью. Классические B-деревья Механизм классических B-деревьев был предложен в 1970 г. Бэйером и Маккрейтом. B-дерево порядка n представляет собой совокупность иерархически связанных страниц внешней памяти (каждая вершина дерева страница), обладающая следующими свойствами: 1. Каждая страница содержит не более 2*n элементов (записей с ключом). 2. Каждая страница, кроме корневой, содержит не менее n элементов. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 30 из 62 [Введите текст] [Введите текст] 3. Если внутренняя (не листовая) вершина B-дерева содержит m ключей, то у нее имеется m+1 страниц-потомков. 4. Все листовые страницы находятся на одном уровне. Пример B-дерева степени 2 глубины 3 приведен на рисунке 5.1. Рис.19. Классическое B-дерево порядка 2 Поиск в B-дереве производится очевидным образом. Предположим, что происходит поиск ключа K. В основную память считывается корневая страница B-дерева. Предположим, что она содержит ключи k1, k2, ..., km и ссылки на страницы p0, p1, ..., pm. В ней последовательно (или с помощью какого-либо другого метода поиска в основной памяти) ищется ключ K. Если он обнаруживается, поиск завершен. Иначе возможны три ситуации: 1. Если в считанной странице обнаруживается пара ключей ki и k(i+1) такая, что ki < K < k(i+1), то поиск продолжается на странице pi. 2. Если обнаруживается, что K > km, то поиск продолжается на странице pm. 3. Если обнаруживается, что K < k1, то поиск продолжается на странице p0. Для внутренних страниц поиск продолжается аналогичным образом, пока либо не будет найден ключ K, либо мы не дойдем до листовой страницы. Если ключ не находится и в листовой странице, значит ключ K в B-дереве отсутствует. Включение нового ключа K в B-дерево выполняется следующим образом. По описанным раньше правилам производится поиск ключа K. Поскольку этот ключ в дереве отсутствует, найти его не удастся, и поиск закончится в некоторой листовой странице A. Далее возможны два случая. Если A содержит менее 2*n ключей, то ключ K просто помещается на свое место, определяемое порядком сортировки ключей в странице A. Если же страница A уже заполнена, то работает процедура расщепления. Заводится новая страница C. Ключи из страницы A (берутся 2*n-1 ключей) + ключ K поровну распределяются между A и C, а средний ключ вместе со ссылкой на страницу УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 31 из 62 [Введите текст] C переносится в непосредственно родительскую страницу B. Конечно, страница B может оказаться переполненной, рекурсивно сработает процедура расщепления и т.д., вообще говоря, до корня дерева. Если расщепляется корень, то образуется новая корневая вершина, и высота дерева увеличивается на единицу. Одношаговое включение ключа с расщеплением страницы показано на рисунке 5.2. (a) Попытка вставить ключ 23 в уже заполненную страницу (b) Выполнение включения ключа 22 путем расщепления страницы A Рис. 20. Пример включения ключа в B-дерево Процедура исключения ключа из классического B-дерева более сложна. Приходится различать два случая - удаление ключа из листовой страницы и удаления ключа из внутренней страницы B-дерева. В первом случае удаление производится просто: ключ просто исключается из списка ключей. При удалении ключа во втором случае для сохранения корректной структуры Bдерева его необходимо заменить на минимальный ключ листовой страницы, к которой ведет последовательность ссылок, начиная от правой ссылки от ключа K (это минимальный содержащийся в дереве ключ, значение которого больше значения K). Тем самым, этот ключ будет изъят из листовой страницы (рисунок 5.3). УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 32 из 62 [Введите текст] [Введите текст] (a) Начальный вид B-дерева (b) B-дерево после удаления ключа 25 Рис. 21. Пример исключения ключа из B-дерева Поскольку в любом случае в одной из листовых страниц число ключей уменьшается на единицу, может нарушиться то требование, что любая, кроме корневой, страница B-дерева должна содержать не меньше n ключей. Если это действительно случается, начинает работать процедура переливания ключей. Берется одна из соседних листовых страниц (с общей страницейпредком); ключи, содержащиеся в этих страницах, а также средний ключ страницы-предка поровну распределяются между листовыми страницами, и новый средний ключ заменяет тот, который был заимствован у страницыпредка (рисунок 22). Рис.22. Результат удаления ключа 38 из B-дерева с рисунка 5.3 Может оказаться, что ни одна из соседних страниц непригодна для переливания, поскольку содержат по n ключей. Тогда выполняется процедура слияния соседних листовых страниц. К 2*n-1 ключам соседних листовых страниц добавляется средний ключ из страницы-предка (из страницы-предка он изымается), и все эти ключи формируют новое содержимое исходной листовой страницы. Поскольку в странице-предке число ключей уменьшилось на единицу, может оказаться, что число элементов в ней стало меньше n, и тогда на этом уровне выполняется процедура переливания, а возможно, и слияния. Так может продолжаться до УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 33 из 62 [Введите текст] внутренних страниц, находящихся непосредственно под корнем B-дерева. Если таких страниц всего две, и они сливаются, то единственная общая страница образует новый корень. Высота дерева уменьшается на единицу, но по-прежнему длина пути до любого листа одна и та же. Пример удаления ключа со слиянием листовых страниц показан на рисунке 5.5. (a) Начальный вид B-дерева (b) B-дерево после удаления ключа 29 Рис. 23. Пример удаления ключа из B-дерева со слиянием листовых страниц УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 34 из 62 [Введите текст] [Введите текст] ПРАКТИЧЕСКИЕ ЗАНЯТИЯ Практическая работа № 1 Сортировка «пузырьком» Данный метод относится к классу простейших, занимая в нем последнее место по производительности. Тем не менее, он очень широко известен, видимо, благодаря своему одному легко запоминающемуся названию – метод всплывающего пузырька (или тонущего шарика, если кому-то так больше нравится). Работа алгоритма действительно похожа на всплывание наверх пузырьков воздуха: сначала на самый верх всплывает самый легкий элемент, потом за ним – чуть более тяжелый и т.д. Пусть имеется n элементов а1 а2, а3, . . ., аn, расположенных в ячейках массива. Для простоты будем считать, что сам элемент совпадает с его ключом. Алгоритм состоит в повторении n-1 шага, на каждом из которых в оставшемся необработанном наборе за счет попарного сравнения соседних элементов отыскивается минимальный элемент. Шаг 1. Сравниваем аn с аn-1 и если аn < аn-1 то меняем их местами, потом сравниваем аn-1 с аn-2 и, возможно, переставляем их, сравниваем аn-2 и аn-3 и т.д. до сравнения и, возможно, перестановки а2 и а1. В результате на первом месте в массиве оказывается самый минимальный элемент, который в дальнейшей сортировке не участвует Шаг 2. Аналогично сравниваем аn с аn-1, аn-1 с аn-2 и т.д., а3 с а2, в результате чего на месте а2 оказывается второй наименьший элемент, который вместе с а1 образует начальную часть упорядоченного массива Шаг 3. Аналогичными сравнениями и перестановками среди элементов а3, а4, … , аn находится наименьший, который занимает место а3 УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 35 из 62 [Введите текст] [Введите текст] ..... Шаг n-1. К этому моменту первые n-2 элемента в массиве уже упорядочены и остается “навести порядок” только между двумя последними элементами аn-1 и аn. На этом сортировка заканчивается. Пример. Дано 6 элементов – целые числа 15, 33, 42, 07, 12, 19. а1 15 15 15 15 07 а2 33 33 33 07 15 а3 42 42 07 33 33 а4 07 07 42 42 42 а5 12 12 12 12 12 а6 19 19 19 19 19 шаг 2 07 07 07 07 15 15 15 12 33 33 12 15 42 12 33 33 12 42 42 42 19 19 19 19 шаг 3 07 07 07 12 12 12 15 15 15 33 19 19 19 33 33 42 42 42 шаг 4 07 07 12 12 15 15 19 19 33 33 42 42 шаг 5 07 12 15 19 33 42 07 12 15 19 33 42 шаг 1 Выполняемые операции сравнение 19 и 12, обмена нет сравнение 12 и 07, обмена нет сравнение 07 и 42, меняем их сравнение 07 и 33, меняем их сравнение 07 и 15, меняем их; 07 – наименьший сравнение 19 и 12, обмена нет сравнение 12 и 42, меняем их сравнение 12 и 33, меняем их сравнение 12 и 15, меняем их, 12 –второй наим. сравнение 19 и 42, меняем их сравнение 19 и 33, меняем их сравнение 19 и 15, обмена нет, 15 – третий наим. сравнение 42 и 33, обмена нет сравнение 33 и 19, обмена нет, 19 – четвертый элем. сравнение 42 и 33, обмена нет, сортировка закончена Итого, для шести элементов сделано 5+4+3+2+1 = 15 сравнений и 8 перестановок. В общем случае, на каждом из n-1 шагов выполняется в среднем n/2 сравнений, поэтому оценка для числа сравнений выражается соотношением n(n-1)/2, т.е. данный метод относится к классу O(n2). Аналогично, число УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 36 из 62 [Введите текст] перестановок тоже пропорционально [Введите текст] n . Несмотря на то, что было 2 предложено несколько улучшений данного метода (есть очень красивые названия – например, шейкер-сортировка), он остается самым неэффективным. Уже для 1000 элементов число сравнений выражается внушительной величиной порядка 500 тысяч. Программная реализация включает двойной цикл: внешний реализует основные шаги алгоритма, внутренний сравнивает и переставляет элементы, начиная с конца массива. for i := 2 to n do begin for j := n downto i do if a[j-1] > a[j] then begin temp := a[j-1]; a[j-1] := a[j]; a[j] := temp; end; end; Практическая работа № 2 Метод простого выбора Данный метод из группы простейших имеет лучшие характеристики по числу перестановок, хотя он, как и оба ранее рассмотренных метода, в целом имеет трудоемкость O(n2). Его чуть более лучшие показатели связаны с тем, что в некоторых ситуациях выполняется перестановка не соседних элементов, а отстоящих на некотором расстоянии друг от друга. Пусть имеется n элементов а1 а2, а3, . . ., аn, расположенных в ячейках массива. Сортировка выполняется за (n–1) шаг, пронумерованных от 1 до n-1. На каждом i-ом шаге обрабатываемый набор разбивается на 2 части: УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 37 из 62 [Введите текст] левую часть образуют уже упорядоченные на предыдущих шагах элементы а1, а2, а3, . . ., аi-1 правую часть образуют еще не обработанные элементы аi, аi+1, аi+2, . . ., аn Суть метода состоит в том, что в необработанном наборе отыскивается наименьший элемент, который меняется местами с элементом аi. На первом шаге (при i = 1), когда необработанным является весь исходный набор, это сводится к поиску наименьшего элемента в массиве и обмену его с первым элементом. Ясно, что поиск наименьшего элемента выполняется обычным попарным сравнением, но соседние элементы при этом не переставляются, что в целом уменьшает число пересылок. Пример. Возьмем тот же исходный набор целых чисел: 15-33-42-07-12-19 шаг 1 шаг 2 шаг 3 шаг 4 шаг 5 а1 15 15 15 15 15 07 07 07 07 07 07 07 07 07 07 а2 33 33 33 33 33 33 33 33 33 12 12 12 12 12 12 а3 42 42 42 42 42 42 42 42 42 42 42 42 15 15 15 а4 07 07 07 07 07 15 15 15 15 15 15 15 42 42 19 а5 12 12 12 12 12 12 12 12 12 33 33 33 33 33 33 а6 19 19 19 19 19 19 19 19 19 19 19 19 19 19 42 Выполняемые операции сравнение 15 и 33, min = 15 сравнение 15 и 42, min = 15 сравнение 15 и 07, min = 07 сравнение 07 и 12, min = 07 сравнение 07 и 19, min = 07, обмен 15 и 07 сравнение 33 и 42, min = 33 сравнение 33 и 15, min = 15 сравнение 15 и 12, min = 12 сравнение 12 и 19, min = 12, обмен 33 и 12 сравнение 42 и 15, min = 15 сравнение 15 и 33, min = 15 сравнение 15 и 19, min = 15, обмен 42 и 15 сравнение 42 и 33, min = 33 сравнение 33 и 19, min = 19, обмен 42 и 19 сравнение 33 и 42, min = 33, обмена нет, все готово УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 38 из 62 [Введите текст] В данном примере сделано 15 сравнений (как и в методе пузырька), но всего 4 перестановки. Эта особенность сохраняется и в целом: по числу сравнений метод выбора близок к методу пузырька, но по числу перестановок существенно превосходит оба рассмотренные выше методы (оценка числа перестановок n*log 2 n) Особенности программной реализации. Внешний цикл обрабатывает основные шаги и выполняет перестановку минимального элемента, а внутренний организует поиск наименьшего элемента в необработанной части массива for i := 1 to n–1 do begin k := i; temp := a [i]; {устанавливаем начальный минимальный элемент} for j := i+1 to n do if a [j] < temp then begin {изменяем текущий минимальный элемент} k := j; temp := a [j]; end; a [k] := a [i]; a [i] := temp; end; Общее заключение по простейшим методам сортировки. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 39 из 62 [Введите текст] [Введите текст] Метод обмена (пузырька) имеет единственное преимущество – нулевое число пересылок в случае, если исходный набор уже отсортирован в нужном порядке. В остальных случаях все его показатели пропорциональны n2. Метод вставок также дает хорошие результаты для упорядоченных входных данных (число сравнений и пересылок пропорционально n). Во всех остальных случаях его показатели пропорциональны n2, хотя что касается оценки среднего числа сравнений, то она чуть лучше, чем у других методов. Многочисленные эксперименты показывают, что метод вставок дает наименьшее время сортировки среди всех простейших методов. Метод выбора, как это и следовало ожидать, имеет лучшие показатели по числу пересылок, особенно – для общего случая, где оценка О(n*log 2 n) заметно лучше оценки O(n2). Поэтому его можно рекомендовать к использованию из всех простейших методов в том случае, если именно число перестановок является наиболее важным. Практическая работа № 3 Метод Шелла. Метод Шелла является улучшенным вариантом метода вставок. Поскольку метод вставок дает хорошие показатели качества для небольших или почти упорядоченных наборов данных, метод Шелла использует эти свойства за счет многократного применения метода вставок. Алгоритм метода Шелла состоит в многократном повторении двух основных действий: объединение нескольких элементов исходного массива по некоторому правилу сортировка этих элементов обычным методом вставок УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 40 из 62 [Введите текст] [Введите текст] Более подробно, на первом этапе группируются элементы входного набора с достаточно большим шагом. Например, выбираются все 1000-е элементы, т.е. создаются группы: группа 1: 1, 1001, 2001, 3001 и т.д. группа 2: 2, 1002, 2002, 3002 и т.д. группа 3: 3, 1003, 2003, 3003 и т.д. ..................... группа 1000: 1000, 2000, 3000 и т.д. Внутри каждой группы выполняется обычная сортировка вставками, что эффективно за счет небольшого числа элементов в группе. На втором этапе выполняется группировка уже с меньшим шагом, например - все сотые элементы. В каждой группе опять выполняется обычная сортировка вставками, которая эффективна за счет того, что после первого этапа в каждой группе набор данных будет уже частично отсортирован. На третьем этапе элементы группируются с еще меньшим шагом, например – все десятые элементы. Выполняется сортировка, группировка с еще меньшим шагом и т.д. На последнем этапе сначала выполняется группировка с шагом 1, создающая единственный набор данных размерности n, а затем - сортировка практически отсортированного набора. Пример. Исходный набор: 15 – 33 – 42 – 07 – 12 - 19 УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 41 из 62 [Введите текст] [Введите текст] Выполняем группировку с шагом 3, создаем три группы по 2 элемента и сортируем каждую из них отдельно: группа 1: 15 – 07 => 07 – 15 (1 сравнение, 1 пересылка) группа 2: 33 – 12 => 12 – 33 (1 сравнение, 1 пересылка) группа 3: 42 – 19 => 19 – 42 (1 сравнение, 1 пересылка) Новый набор чисел: 07 – 15 – 12 – 33 – 19 – 42 Группировка с меньшим шагом 2 дает 2 группы по 3 элемента, которые сортируются отдельно: группа 1: 07 – 12 – 19 => уже упорядочена (2 сравнения, 0 пересылок) группа 2: 15 – 33 – 42 => уже упорядочена (2 сравнения, 0 пересылок) Новый набор чисел: 07 – 12 – 19 – 15 – 33 – 42 Последняя группировка с шагом 1 дает сам набор чисел; к нему применяется сортировка вставками с 5-ю сравнениями и только одной пересылкой, после чего получаем искомый результат. Итого – 12 сравнений и 4 пересылки, что в общем-то не лучше чем у простых методов. Однако, здесь надо учесть два фактора. Фактор 1 (общий). Улучшенные методы показывают свою эффективность именно для больших наборов данных (сотни, тысячи и т.д. элементов). Для очень малых наборов (как в примере) они могут давать даже худшие результаты. Фактор 2 (специфический). Эффективность метода Шелла существенно зависит от выбора последовательности шагов группировки. Эта УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 42 из 62 [Введите текст] [Введите текст] последовательность обязательно должна быть убывающей, а последний шаг обязательно равен 1. В настоящее время неизвестна наилучшая последовательность шагов, обеспечивающая наименьшую трудоемкость. На основе многочисленных экспериментов установлено, что число шагов группировки надо выбирать по формуле [(log 2 n)] – 1, где скобки [ ] используются для обозначения целой части числа, а в качестве самих последовательностей рекомендуется один из следующих наборов (обращаю внимание: для удобства восприятия шаги даются в обратном порядке): 1, 3, 5, 9, 17, 33, . . . (общая формула: tk = (2* tk-1) –1 ) 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767 . . . (общая формула: tk = (2* tk-1) +1, а еще проще – (2k – 1)). В соответствии с этими рекомендациями, в предыдущем примере надо взять лишь 2 шага группировки со значениями 3 и 1. В этом случае потребуется лишь 8 сравнений и 5 пересылок. Что касается программной реализации, то по сравнению с методом вставок потребуется организовать еще один самый внешний цикл для выполнения группировок элементов с убывающими шагами. Сами шаги можно вычислять по приведенным выше формулам, а можно хранить в предварительно выделения подготовленном сгруппированных вспомогательном элементов в массиве. отдельные Никакого массивы не производится, вся работа выполняется за счет изменения индексов элементов. for m := 1 to t do {t – число шагов группировки, m – номер шага} begin k := h [m]; {выбор величины шага группировки из заданного массива} УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] for i := k + 1 to n do Страница 43 из 62 [Введите текст] [Введите текст] {сортировка вставками внутри каждой группы} begin temp := a [i]; j := i – k; while (j > 0) and (temp < a [j]) do begin a [j + k] := a [j]; j := j – k; end; a [j + k] := temp; end; end; Оценка трудоемкости метода Шелла выражается соотношением O(n1,2), что лучше чем у простейших методов, особенно при больших n. Практическая работа № 4 Быстрая сортировка Данный метод в настоящее время считается наиболее быстрым универсальным методом сортировки. Как ни странно, он является обобщением самого плохого из простейших методов – обменного метода. Эффективность метода достигается тем, что перестановка применяется не для соседних элементов, а отстоящих друг от друга на приличном расстоянии. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Более Страница 44 из 62 [Введите текст] конкретно, алгоритм быстрой [Введите текст] сортировки заключается в следующем. пусть каким-то образом в исходном наборе выделен некий элемент x, который принято называть опорным. В простейшем случае в качестве опорного можно взять серединный элемент массива просматривается часть массива, расположенная левее опорного элемента и находится первый по порядку элемент ai > x после этого просматривается часть массива, расположенная правее опорного элемента, причем - в обратном порядке, и находится первый по порядку (с конца) элемент aj < x производится перестановка элементов ai и aj после этого в левой части, начиная с ai отыскивается еще один элемент, больший x, а в правой части, начиная с aj отыскивается элемент, меньший х эти два элемента меняются местами эти действия (поиск слева и справа с последующим обменом) продолжаются до тех пор, пока не будет достигнут опорный элемент x после этого слева от опорного элемента x будут находиться элементы, меньшие опорного, а справа – элементы, большие опорного. При этом обе половины скорее всего не будут отсортированными после этого массив разбивается на правую и левую части и каждая часть обрабатывается отдельно по той же самой схеме: определение УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 45 из 62 [Введите текст] опорного элемента, поиск слева и справа соответствующих элементов и их перестановка и т.д. Пример. Пусть исходный набор включает 11 чисел: 13-42-28-17-09-2547-31-39-15-20. Основные шаги сортировки: 1. Выбор серединного элемента 25 (индекс 6): 13 42 28 17 09 25 47 31 39 15 20 2. поиск слева первого элемента, большего 25: 42 (2 сравнения) 3. поиск справа от конца первого элемента, меньшего 25: 20 (1 сравнение) 4. перестановка элементов 42 и 20: 13 20 28 17 09 25 47 31 39 15 42 5. поиск слева от 25 еще одного элемента, большего 25: 28 (1 сравнение) 6. поиск справа от 25 еще одного элемента, меньшего 25: 15 (1 сравнение) 7. перестановка элементов 28 и 15: 13 20 15 17 09 25 47 31 39 28 42 8. поиск слева от 25 еще одного элемента, большего 25: нет (2 сравнения) 9. поиск справа от 25 еще одного элемента, меньшего 25: нет (3 сравнения) УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 46 из 62 [Введите текст] [Введите текст] 10. теперь слева от 25 все элементы меньше 25, а справа – больше 11. выделяем отдельно левую часть: 13 20 15 17 09 12. выбираем серединный элемент 15 (индекс 3): 13 20 15 17 09 13. поиск слева от 15 элемента, большего 15: 20 (2 сравнения) 14. поиск справа от 15 элемента, меньшего 15: 09 (1 сравнение) 15. перестановка 20 и 09: 13 09 15 17 20 16. поиск справа от 15 еще одного элемента, меньшего 15: нет (1 сравнение) 17. теперь слева от 15 все элементы меньше 15, а справа – больше 18. поскольку слева от 15 только 2 элемента, просто сравниваем их друг с другом и переставляем (09 и 13) 19. поскольку справа от 15 только 2 элемента, просто сравниваем их и не переставляем 20. получаем для левой части упорядоченный набор: 09 13 15 17 20 21. возвращаемся к правой части: 47 31 39 28 42 22. выделяем серединный элемент 39 (индекс в данном поднаборе – 3): 47 31 39 28 42 23. поиск слева от 39 элемента, большего 39: 47 (1 сравнение) 24. поиск справа от 39 элемента, меньшего 39: 28 (2 сравнения) 25. переставляем 47 и 28: 28 31 39 47 42 УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 47 из 62 [Введите текст] 26. поиск слева от 39 еще одного элемента, большего 39: нет (1 сравнение) 27. теперь слева от 39 все элементы меньше 39, а справа – больше 28. поскольку слева от 39 только 2 элемента, просто сравниваем их и не переставляем 29. поскольку справа от 39 только 2 элемента, просто сравниваем их и переставляем (42 и 47) 30. получаем для правой части упорядоченный набор: 28 31 39 42 47 31. вместе с левой частью и серединным элементом 25 получаем окончательный результат Итого для данного примера потребовалось 22 сравнения и 6 пересылок. В целом, оценка трудоемкости метода быстрой сортировки является типичной для улучшенных методов и выражается соотношением (n*log 2 n)/6. Отсюда следует, что данный метод неэффективен при малых n (десятки или сотни элементов), но с ростом n его эффективность резко растет, и при очень больших n метод дает наилучшие показатели среди всех универсальных методов сортировки. К сожалению, есть одна ситуация, когда быстрая сортировка теряет свою эффективность и становится пропорциональной n2, т.е. опускается до уровня простых методов. Эта ситуация связана с правилом выбора опорного элемента. Эффективность метода сильно зависит от выбора опорного элемента, и использование простейшего способа выбора (серединный элемент массива) часто приводит к падению эффективности метода. Это связано с тем, что каждое разделение массива на две половины в УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 48 из 62 [Введите текст] идеале должно давать примерно равное число элементов слева и справа от опорного элемента (принцип дихотомии!). Если опорный элемент близок к минимальному или максимальному, после попарных перестановок будут получены существенно неравномерные наборы. Если подобная ситуация возникает на каждом шаге работы алгоритма, общая эффективность резко падает. Для устранения этого недостатка надо уметь правильно выбирать опорный элемент. Наилучшее правило выбора опорного элемента – это так называемая медиана. Медиана – это средний элемент массива не по расположению, а по значению. В приведенном выше примере медианой является число 25, которое также было и серединным элементом (честно говоря, пример был подобран специально). К сожалению, поиск медианы в массиве является задачей, сопоставимой по трудоемкости с самой сортировкой, поэтому были предложены другие, более простые правила выбора опорного элемента. На практике хорошо показал себя следующий способ: выбрать случайно в массиве три элемента и взять в качестве опорного средний из них. Этот способ очень прост в реализации, т.к. требует только двух сравнений, но, конечно, он может обеспечивать хорошие показатели только в среднем, и не гарантирует идеальное поведение алгоритма абсолютно для ЛЮБЫХ входных данных. Есть и другие усовершенствования базового алгоритма быстрой сортировки. Среди них: Проверка каждого подмассива на возможную упорядоченность перед самой сортировкой Для малых подмассивов (n < 10) разумно проводить сортировку с помощью более простых методов, например – Шелла УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 49 из 62 [Введите текст] [Введите текст] Комбинация всех этих методов известна как метод Синглтона [7]. Что касается программной реализации базового алгоритма, то можно заметить его принципиальную особенность: разделение массива на 2 половины, разделение каждой половины на свои половины и т.д. При каждом разделении приходится запоминать правую половину (конечно, не сами элементы, а лишь индексы левой и правой границы) и возвращаться к ней после полной обработки левой половины. Все это как нельзя лучше соответствует рекурсивному принципу обработки, и поэтому быстрая сортировка проще всего реализуется рекурсивно. Конечно, при необходимости можно включить явное использование стека для запоминания левой и правой границы подмассива, аналогично нерекурсивной реализации процедур обхода дерева. Рекурсивная реализация с серединным опорным элементом схематично выглядит следующим образом: Procedure QuickSort (left, right : integer); {формальные параметры для запоминания границ} var i, j : integer; sred, temp : <описание элемента исходного массива>; begin i := left; j := right; {установка начальных значений границ подмассива} sred := mas [(left + right) div 2]; {определение серединного элемента} repeat УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 50 из 62 [Введите текст] [Введите текст] while (mas [i] < sred) do i := i + 1; {поиск слева элемента, большего опорного} while (mas [j] > sred) do j := j – 1; {поиск справа элемента, меньшего опорного} if i <= j then begin {обмениваем элементы и изменяем индексы} temp := mas [i]; mas [i] := mas [j]; mas [j] := temp; i := i + 1; j := j –1; end; until i > j; if left < j then QuickSort (left , j); {обработка левой половины} if i < right then QuickSort (i , right); {обработка правой половины} end; Первоначальный вызов этой процедуры производится в главной программе в виде QuickSort (1, n); здесь n – число элементов в массиве. Практическая работа № 5 Сортировка модифицированным методом простого выбора Пусть имеется набор из n элементов а1, а2, а3, . . ., аn с некоторыми ключами (как и раньше, для простоты будем считать, что сам элемент совпадает с его ключом). Требуется этот набор организовать в виде некоторой структуры данных с возможностью многократного поиска в нем УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 51 из 62 [Введите текст] [Введите текст] элементов с заданным ключом. Эта задача может решаться различными способами: если набор элементов никак не упорядочен, то поиск выполняется прямым сравнением всех элементов в массиве или списке с трудоемкостью O(n) если элементы упорядочены в массиве или в дереве поиска, поиск более эффективно выполняется как двоичный, с трудоемкостью О(log 2 n) Возникает вопрос: существуют ли еще более эффективные методы поиска? Оказывается, при выполнении некоторых дополнительных условий можно организовать исходный набор ключей в виде специальной структуры данных, называемой хеш-таблицей, поиск в которой ЛЮБОГО элемента в идеале выполняется за ОДНО сравнение и НЕ зависит от размерности входного набора. Другими словами, трудоемкость такого метода поиска, называемого хеш-поиском, пропорциональна О(1), что является абсолютным рекордом! Метод хеш-поиска заключается в следующем. Исходные элементы а1, а2, а3, . . ., аn распределяются некоторым специальным образом по ячейкам массива. Пока будем считать, что число ячеек массива m > n. Идеальным поиском можно считать такой, когда по любому входному ключу сразу вычисляется индекс ячейки с этим ключом, без проверки содержимого остальных ячеек. Для вычисления индекса ячейки по входному ключу используется специальная функция, называемая хеш-функцией. Эта функция ставит в соответствие каждому ключу индекс ячейки массива, где должен располагаться элемент с этим ключом: h (аi ) = j, j = (1, m); УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 52 из 62 [Введите текст] [Введите текст] Массив, заполненный элементами исходного набора в порядке, определяемом хеш-функцией, называется хеш-таблицей. Отсюда следует, что решение задачи поиска данным методом во многом зависит от используемой хеш-функции. Предложено довольно много различных хеш-функций. Самой простой, но не самой лучшей хеш-функцией является функция взятия остатка от деления ключа нацело на m: h (аi ) = (аi mod m) + 1; Ясно, что каждое значение этой функции лежит в пределах от 1 до m и может приниматься в качестве индекса ячейки массива. Принято считать, что хорошей является хеш-функция, которая удовлетворяет следующим условиям: функция должна быть очень простой с вычислительной точки зрения функция должна распределять ключи в хеш-таблице как можно более равномерно Использование данного метода включает два этапа: построение хеш-таблицы для заданного набора ключей с помощью выбранной хеш-функции, т.е. определение для каждого ключа его местоположения в таблице использование построенной таблицы для поиска элементов с помощью той же самой хеш-функции Рассмотрим два примера с целыми и строковыми ключами. Пример 1. Пусть задан набор из 8 целочисленных ключей: УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 53 из 62 [Введите текст] 35, 19, 07, 14, 26, 40, 51, 72. Требуется распределить эти ключи в массиве из 10 ячеек с помощью простейшей хеш-функции. Для этого каждый ключ делим нацело на 10 и используем остаток в качестве индекса размещения ключа в массиве: 35 mod 10 = 5, индекс размещения ключа 35 равен 6 19 mod 10 = 9, индекс размещения ключа 19 равен 10 07 mod 10 = 7, индекс размещения ключа 07 равен 8 14 mod 10 = 4, индекс размещения ключа 14 равен 5 26 mod 10 = 6, индекс размещения ключа 26 равен 7 40 mod 10 = 0, индекс размещения ключа 40 равен 1 51 mod 10 = 1, индекс размещения ключа 51 равен 2 72 mod 10 = 2, индекс размещения ключа 72 равен 3 Получаем следующую хеш-таблицу: индекс 1 ключ 40 2 51 3 72 4 5 14 6 35 7 26 8 07 9 10 19 Если требуется найти в этой хеш-таблице ключ со значением 26, то этот поиск выполняется ровно за одно сравнение: делим 26 на 10, берем остаток 6, входим в ячейку с индексом 7 и сравниваем находящееся там значение с заданным ключом. Пример 2. Пусть ключи являются строковыми. В этом случае предварительно текстовый ключ надо преобразовать в числовой. Например, можно сложить ASCII-коды всех символов, входящих в этот текстовый ключ. УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 54 из 62 [Введите текст] [Введите текст] Например, если строковый ключ имеет значение END, то его целочисленный эквивалент будет равен сумме кодов всех трех символов: ord(E) + ord(N) + ord(D) = 69 + 78 + 68 = 215 Тогда для четырех строковых ключей, являющихся служебными словами языка Паскаль, получим следующие значения простейшей хешфункции, определяющие размещение этих ключей в десятиэлементной хештаблице: h (END) = (215 mod 10) + 1 = 6 h (VAR) = (233 mod 10) + 1 = 4 h (AND) = (211 mod 10) + 1 = 2 h (NIL) = (227 mod 10) + 1 = 8 В результате для этих четырех строковых ключей получаем следующую хештаблицу: индекс 1 2 3 4 5 6 7 8 9 ключ AND VAR END NIL 10 Поиск в этой таблице некоторого ключа выполняется очень просто: находится целочисленный эквивалент строкового ключа, вычисляется значение хеш-функции и сравнивается содержимое полученной ячейки с заданным ключом. Например, h (VAR) = 4, сравниваем содержимое ячейки 4 с ключом VAR, фиксируем совпадение и завершаем поиск с признаком успеха. Приведенные выше примеры носят несколько искусственный характер, поскольку они описывают идеальный случай, когда хеш-функция для всех УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 55 из 62 [Введите текст] [Введите текст] различных ключей дает РАЗЛИЧНЫЕ значения индексов в массиве. В этом случае каждый ключ имеет свое уникальное расположение в массиве, не конфликтуя с другими ключами. Подобная ситуация возможна, если исходный набор ключей известен заранее и после построения хеш-таблицы не изменяется, т.е. ключи НЕ добавляются и НЕ удаляются из хеш-таблицы. В этом случае за счет подбора хеш-функции и, возможно, небольшого изменения самих ключей можно построить бесконфликтную хеш-таблицу. Важным практическим примером такой ситуации является построение таблицы ключевых слов в программах-трансляторах с языков программирования. Здесь набор ключевых слов является постоянным, изменяясь только при изменении версии транслятора, а с другой стороны, обработка транслятором входного текста на языке программирования требует многократного и очень быстрого распознавания в этом тексте ключевых слов языка. К сожалению, идеальный случай возможен весьма редко, и ограничивать применение хеш-поиска только данным случаем было бы неразумно, учитывая выдающиеся потенциальные скоростные возможности метода. Поэтому были предложены различные усовершенствования хеш-поиска, существенно расширившие область его использования. Эти усовершенствования так или иначе связаны с обработкой конфликтных ситуаций, когда два РАЗНЫХ ключа претендуют на ОДНО и то же место в хеш-таблице, т.е. хеш-функция дает для этих разных ключей аi и ак одно и то же значение: h (аi ) = h (ак ) = j Например, в приведенном выше примере со строковыми ключами простая перестановка символов в ключе приводит к конфликту: h (VAR) = h (RAV) = h (AVR) = (233 mod 10) + 1 = 4 УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 56 из 62 [Введите текст] [Введите текст] Для разрешения конфликтов были предложены разные методы, которые можно сгруппировать в две основные группы – открытое хеширование и внутреннее хеширование (необходимо отметить, что данная терминология не является общепринятой и допускает разночтения, поэтому в первую очередь надо обращать внимание на сущность метода разрешения конфликтов). Практическая работа № 6 Компиляция приложения. Виды компиляции. Базовым элементом Б-дерева является страница, поскольку именно она содержит всю основную информацию. Поэтому, прежде всего, необходимо описать структуру страницы Б-дерева. Каждая страница должна содержать следующие данные: текущее количество элементов на странице (оно изменяется от m до 2m) указатель на страницу, являющуюся самым левым потомком данной страницы основной массив элементов страницы, размерность массива 2m, каждый элемент – это запись со следующими полями: o ключ некоторого типа o указатель на страницу, являющуюся потомком текущей страницы и содержащую ключи, большие ключа данного элемента o информационная часть (как некоторая структура или указатель на область памяти) УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 57 из 62 [Введите текст] [Введите текст] Соответствующие описания типов можно сделать следующим образом: type pPage = ^Page; TItem = record {ссылочный тип для адресации страниц} {описание структуры элемента массива} key : integer; cPage : pPage; {ключ вершины дерева} {указатель на страницу-потомка} inf : <описание информационной части>; end; Page = record {описание структуры страницы} nPage : word; leftPage : pPage; {число вершин на странице} {указатель на самого левого потомка} mas : array [1 . . 2m] of TItem; {основной массив} end; Из переменных достаточно ввести два указателя: на корневую страницу (pRoot) и текущую обрабатываемую в данный момент страницу (pCurrent). Схематично структуру страницы можно представить следующим образом (для краткости указатель pCurrent заменен идентификатором pC): УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] pC^.mas[1] pC^.nPage pC^.leftPage Страница 58 из 62 .key .cPage [Введите текст] pC^.mas[2] .inf .key .cPage ..... .inf pC^.mas[2m] .key .cPage .inf Видно, что Б-дерево является достаточно сложной комбинированной структурой, представляя собой набор связанных указателями записей. Для использования Б-дерева необходим стандартный набор операций: поиск заданного элемента, добавление вершины, удаление вершины. Практическая работа № 7 Сортировка массивов При работе с массивами данных не редко возникает задача их сортировки по возрастанию или убыванию, т.е. упорядочивания. Это значит, что элементы того же нужно расположить строго по порядку. Например, в случае сортировки по возрастанию предшествующий элемент должен быть меньше последующего (или равен ему). Алгоритм решения задачи: Существует множество методов сортировки. Одни из них являются более эффективными, другие – проще для понимания. Достаточно простой для понимания является сортировка методом пузырька, который также называют методом простого обмена. В чем же он заключается, и почему у него такое странное название: "метод пузырька"? Как известно воздух легче воды, поэтому пузырьки воздуха всплывают. Это просто аналогия. В сортировке методом пузырька по возрастанию более легкие (с меньшим значением) элементы постепенно "всплывают" в начало УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Страница 59 из 62 [Введите текст] [Введите текст] массива, а более тяжелые друг за другом опускаются на дно (в конец массива). Алгоритм и особенности этой сортировки таковы: 1. При первом проходе по массиву элементы попарно сравниваются между собой: первый со вторым, затем второй с третьим, следом третий с четвертым и т.д. Если предшествующий элемент оказывается больше последующего, то их меняют местами. 2. Не трудно догадаться, что постепенно самое большое число оказывается последним. Остальная часть массива остается не отсортированной, хотя некоторое перемещение элементов с меньшим значением в начало массива наблюдается. 3. При втором проходе незачем сравнивать последний элемент с предпоследним. Последний элемент уже стоит на своем месте. Значит, число сравнений будет на одно меньше. 4. На третьем проходе уже не надо сравнивать предпоследний и третий элемент с конца. Поэтому число сравнений будет на два меньше, чем при первом проходе. 5. В конце концов, при проходе по массиву, когда остаются только два элемента, которые надо сравнить, выполняется только одно сравнение. 6. После этого первый элемент не с чем сравнивать, и, следовательно, последний проход по массиву не нужен. Другими словами, количество проходов по массиву равно m-1, где m – это количество элементов массива. 7. Количество сравнений в каждом проходе равно m-i, где i – это номер прохода по массиву (первый, второй, третий и т.д.). УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] Страница 60 из 62 [Введите текст] 8. При обмене элементов массива обычно используется "буферная" (третья) переменная, куда временно помещается значение одного из элементов. Подробнее УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] Подробнее Программа на языке Паскаль: const m = 10; var arr: array[1..m] of integer; i, j, k: integer; begin [Введите текст] Страница 61 из 62 [Введите текст] УМКД 042-14-02-03.1.20.47/3-2013 Редакция №1 от 28.08.2013 г [Введите текст] [Введите текст] randomize; write ('Исходный массив: '); for i := 1 to m do begin arr[i] := random(256); write (arr[i]:4); end; writeln; writeln; for i := 1 to m-1 do for j := 1 to m-i do if arr[j] > arr[j+1] then begin k := arr[j]; arr[j] := arr[j+1]; arr[j+1] := k end; write ('Отсортированный массив: '); for i := 1 to m do write (arr[i]:4); writeln; readln end. Страница 62 из 62 [Введите текст]