Операции логического уровня над статическими структурами. Сортировка
Для самого общего случая сформулируем задачу сортировки таким образом: имеется некоторое неупорядоченное входное множество ключей и необходимо получить выходное множество тех же ключей, упорядоченных по возрастанию или убыванию в численном или лексикографическом порядке.
Из всех задач программирования сортировка, возможно, имеет самый богатый выбор алгоритмов решения. Назовем некоторые факторы, которые влияют на выбор алгоритма (помимо порядка алгоритма).
1). Имеющийся ресурс памяти: должны ли входное и выходное множества располагаться в разных областях памяти или выходное множество может быть сформировано на месте входного. В последнем случае имеющаяся область памяти должна в ходе сортировки динамически перераспределяться между входным и выходным множествами; для одних алгоритмов это связано с большими затратами, для других - с меньшими.
2). Исходная упорядоченность входного множества: во входном множестве (даже если оно сгенерировано датчиком случайных величин) могут попадаться упорядоченные участки. В предельном случае входное множество может оказаться уже упорядоченным. Одни алгоритмы не учитывают исходной упорядоченности и требуют одного и того же времени для сортировки любого (в том числе и уже упорядоченного) множества данного объема, другие выполняются тем быстрее, чем лучше упорядоченность на входе.
3). Временные характеристики операций: при определении порядка алгоритма время выполнения считается обычно пропорциональным числу сравнений ключей. Ясно, однако, что сравнение числовых ключей выполняется быстрее, чем строковых, операции пересылки, характерные для некоторых алгоритмов, выполняются тем быстрее, чем меньше объем записи, и т.п. В зависимости от характеристик записи таблицы может быть выбран алгоритм, обеспечивающий минимизацию числа тех или иных операций.
4). Сложность алгоритма является не последним соображением при его выборе. Простой алгоритм требует меньшего времени для его реализации и вероятность ошибки в реализации его меньше. При промышленном изготовлении программного продукта требования соблюдения сроков разработки и надежности продукта могут даже превалировать над требованиями эффективности функционирования.
Разнообразие алгоритмов сортировки требует некоторой их классификации. Выбран один из применяемых для классификации подходов, ориентированный прежде всего на логические характеристики применяемых алгоритмов. Согласно этому подходу любой алгоритм сортировки использует одну из следующих четырех стратегий (или их комбинацию).
1). Стратегия выборки. Из входного множества выбирается следующий по критерию упорядоченности элемент и включается в выходное множество на место, следующее по номеру.
2). Стратегия включения. Из входного множества выбирается следующий по номеру элемент и включается в выходное множество на то место, которое он должен занимать в соответствии с критерием упорядоченности.
3). Стратегия распределения. Входное множество разбивается на ряд подмножеств (возможно, меньшего объема) и сортировка ведется внутри каждого такого подмножества.
4). Стратегия слияния. Выходное множество получается путем слияния маленьких упорядоченных подмножеств.
Далее приводится обзор (далеко не полный) методов сортировки, сгруппированных по стратегиям, применяемым в их алгоритмах.
Все алгоритмы рассмотрены для случая упорядочения по возрастанию ключей.
Сортировки выборкой
СОРТИРОВКА ПРОСТОЙ ВЫБОРКОЙ. Данный метод реализует практически "дословно" сформулированную выше стратегию выборки. Порядок алгоритма простой выборки - O(N2). Количество пересылок - N.
Алгоритм сортировки простой выборкой иллюстрируется программным примером 3.7.
В программной реализации алгоритма возникает проблема значения ключа "пусто". Довольно часто программисты используют в качестве такового некоторое заведомо отсутствующее во входной последовательности значение ключа, например, максимальное из теоретически возможных значений. Другой, более строгий подход - создание отдельного вектора, каждый элемент которого имеет логический тип и отражает состояние соответствующего элемента входного множества ("истина" - "непусто", "ложь" - "пусто"). Именно такой подход реализован в нашем программном примере. Роль входной последовательности здесь выполняет параметр a, роль выходной - параметр b, роль вектора состояний – массив с. Алгоритм несколько усложняется за счет того, что для установки начального значения при поиске минимума приходится отбрасывать уже "пустые" элементы.
{===== Программный пример 3.7 =====}
Procedure Sort( a : SEQ; var b : SEQ);
Var i, j, m : integer;
c: array[1..N] of boolean; {состояние эл-тов вх.множества}
begin
for i:=1 to N do c[i]:=true; { сброс отметок }
for i:=1 to N do {поиск 1-го невыбранного эл. во вх.множестве}
begin j:=1;
while not c[j] do j:=j+1;
m:=j; { поиск минимального элемента }
for j:=2 to N do
if c[j] and (a[j]<a[m]) then m:=j;
b[i]:=a[m]; { запись в выходное множество }
c[m]:=false; { во входное множество - "пусто"}
end; end;
ОБМЕННАЯ СОРТИРОВКА ПРОСТОЙ ВЫБОРКОЙ. Алгоритм сортировки простой выборкой, однако, редко применяется в том варианте, в каком он описан выше. Гораздо чаще применяется его, так называемый, обменный вариант. При обменной сортировке выборкой входное и выходное множество располагаются в одной и той же области памяти; выходное - в начале области, входное - в оставшейся ее части. В исходном состоянии входное множество занимает всю область, а выходное множество - пустое. По мере выполнения сортировки входное множество сужается, а выходное - расширяется.
Обменная сортировка простой выборкой показана в программном примере 3.8. Процедура имеет только один параметр - сортируемый массив.
{===== Программный пример 3.8 =====}
Procedure Sort(var a : SEQ);
Var x, i, j, m : integer;
begin
for i:=1 to N-1 do { перебор элементов выходного множества}
{ входное множество - [i:N]; выходное - [1:i-1] }
begin m:=i;
for j:=i+1 to N do { поиск минимума во входном множестве }
if (a[j]<a[m]) then m:=j;
{ обмен 1-го элемента вх. множества с минимальным }
if i<>m then begin
x:=a[i]; a[i]:=a[m]; a[m]:=x;
end;end; end;
Результаты трассировки программного примера 3.8 представлены в табл. 3.5. Двоеточием показана граница между входным и выходным множествами.
Очевидно, что обменный вариант обеспечивает экономию памяти. Очевидно также, что здесь не возникает проблемы "пустого" значения. Общее число сравнений уменьшается вдвое - N*(N-1)/2, но порядок алгоритма остается степенным - O(N2). Количество перестановок N-1, но перестановка, по-видимому, вдвое более времяемкая операция, чем пересылка в предыдущем алгоритме.
Таблица 3.5
Шаг
| Содержимое массива а
| Исходный
| : 242 447 286 708 24 11 192 860 937 561
|
| 11:447 286 708 24 242 192 860 937 561
|
| 11 24:286 708 447 242 192 860 937 561
11 24 192:708 447 242 286 860 937 561
11 24 192 242:447 708 286 860 937 561
|
| 11 24 192 242 286:708 447 860 937 561
|
| 11 24 192 242 286 447:708 860 937 561
|
| 11 24 192 242 286 447 561:860 937 708
|
| 11 24 192 242 286 447 561 708:937 860
|
| 11 24 192 242 286 447 561 708 860:937
| результат
| 11 24 192 242 286 447 561 708 860 937
|
Довольно простая модификация обменной сортировки выборкой предусматривает поиск в одном цикле просмотра входного множества сразу и минимума, и максимума и обмен их с первым и с последним элементами множества соответственно. Хотя итоговое количество сравнений и пересылок в этой модификации не уменьшается, достигается экономия на количестве итераций внешнего цикла.
Приведенные выше алгоритмы сортировки выборкой практически нечувствительны к исходной упорядоченности. В любом случае поиск минимума требует полного просмотра входного множества. В обменном варианте исходная упорядоченность может дать некоторую экономию на перестановках для случаев, когда минимальный элемент найден на первом месте во входном множестве.
ПУЗЫРЬКОВАЯ СОРТИРОВКА. Входное множество просматривается, при этом попарно сравниваются соседние элементы множества. Если порядок их следования не соответствует заданному критерию упорядоченности, то элементы меняются местами. В результате одного такого просмотра при сортировке по возрастанию элемент с самым большим значением ключа переместится ("всплывет") на последнее место в множестве. При следующем проходе на свое место "всплывет" второй по величине ключа элемент и т.д. Для постановки на свои места N элементов следует сделать N-1 проходов. Выходное множество, таким образом, формируется в конце сортируемой последовательности, при каждом следующем проходе его объем увеличивается на 1, а объем входного множества уменьшается на 1.
Порядок пузырьковой сортировки - O(N2). Среднее число сравнений - N*(N-1)/2 и таково же среднее число перестановок, что значительно хуже, чем для обменной сортировки простым выбором.
Однако, то обстоятельство, что здесь всегда сравниваются и перемещаются только соседние элементы, делает пузырьковую сортировку удобной для обработки связных списков. Перестановка в связных списках также получается более экономной.
Еще одно достоинство пузырьковой сортировки заключается в том, что при незначительных модификациях ее можно сделать чувствительной к исходной упорядоченности входного множества. Рассмотрим некоторые из таких модификаций.
Во-первых, можно ввести некоторую логическую переменную, которая будет сбрасываться в false перед началом каждого прохода и устанавливаться в true при любой перестановке. Если по окончании прохода значение этой переменной останется false, это означает, что менять местами больше нечего, сортировка закончена. При такой модификации поступление на вход алгоритма уже упорядоченного множества потребует только одного просмотра.
Во-вторых, может быть учтено то обстоятельство, что за один просмотр входного множества на свое место могут "всплыть" не один, а два и более элементов. Это легко учесть, запоминая в каждом просмотре позицию последней перестановки и установки этой позиции в качестве границы между множествами для следующего просмотра. Именно эта модификация реализована в программной иллюстрации пузырьковой сортировке в примере 3.9. Переменная nn в каждом проходе устанавливает верхнюю границу входного множества.
В переменной x запоминается позиция перестановок и в конце просмотра последнее запомненное значение вносится в nn. Сортировка закончена, когда верхняя граница входного множества станет равной 1.
{===== Программный пример 3.9 =====}
Procedure Sort( var a : seq);
Var nn, i, x : integer;
begin nn:=N; { граница входного множества }
repeat x:=1; { признак перестановок }
for i:=2 to nn do { перебор входного множества }
if a[i-1]>a[i] then begin { сравнение соседних эл-в }
x:=a[i-1]; a[i-1]:=a[i]; a[i]:=x { перестановка }
x:=i-1; { запоминание позиции }
end; nn:=x; { сдвиг границы }
until (nn=1); {цикл, пока вых. множество не захватит весь мас.}
end;
Результаты трассировки программного примера 3.9 представлены в табл. 3.6.
Таблица 3.6
Шаг
| nn
| Содержимое а
| Исходный
|
| 717 473 313 160 949 764 34 467 757 800:
|
|
| 473 313 160 717 764 34 467 757 800:949
|
|
| 313 160 473 717 34 467 757:764 800 949
|
|
| 160 313 773 34 467:717 757 764 800 949
|
|
| 160 313 34 467:473 717 757 764 800 949
|
|
| 160 34:313 467 473 717 757 764 800 949
|
|
| 34:160 313 467 473 717 757 764 800 949
| Результат
|
| : 34 160 313 467 473 717 757 764 800 949
|
Еще одна модификация пузырьковой сортировки носит название шейкер-сортировки. Суть ее состоит в том, что направления просмотров чередуются: за просмотром от начала к концу следует просмотр от конца к началу входного множества. При просмотре в прямом направлении запись с самым большим ключом ставится на свое место в последовательности, при просмотре в обратном направлении - запись с самым маленьким. Этот алгоритм весьма эффективен для задач восстановления упорядоченности, когда исходная последовательность уже была упорядочена, но подверглась не очень значительным изменениям. Упорядоченность в последовательности с одиночным изменением будет гарантированно восстановлена всего за два прохода.
СОРТИРОВКА ШЕЛЛА. Это еще одна модификация пузырьковой сортировки. Здесь выполняется сравнение ключей, отстоящих один от другого на некотором расстоянии d. Исходный размер d обычно выбирается соизмеримым с половиной общего размера сортируемой последовательности. Выполняется пузырьковая сортировка с интервалом сравнения d. Затем величина d уменьшается вдвое и вновь выполняется пузырьковая сортировка, далее d уменьшается еще вдвое и т.д.
Последняя пузырьковая сортировка выполняется при d = 1. Качественный порядок сортировки Шелла остается O(N2), среднее же число сравнений, определенное эмпирическим путем, - log2(N2*N). Ускорение достигается за счет того, что выявленные "не на месте" элементы при d>1 быстрее "всплывают" на свои места.
Пример 3.10 иллюстрирует сортировку Шелла.
{===== Программный пример 3.10 =====}
Procedure Sort( var a : seq);
Var d, i, t : integer;
k : boolean; { признак перестановки }
begin d:=N div 2; { начальное значение интервала }
while d>0 do begin { цикл с уменьшением интервала до 1 }
k:=true; { пузырьковая сортировка с интервалом d }
while k do { цикл, пока есть перестановки }
begin k:=false; i:=1;
for i:=1 to N-d do {сравнение эл-тов на интервале d
begin if a[i]>a[i+d] then
begin t:=a[i]; a[i]:=a[i+d]; a[i+d]:=t; {перестановка }
k:=true; { признак перестановки }
end; { if ... } end; { for ... } end; { while k }
d:=d div 2; { уменьшение интервала }
end; { while d>0 } end;
Результаты трассировки программного примера 3.10 представлены в табл. 3.7.
Таблица 3.7
Шаг
| d
| Cодержимое массива а
| Исходный
|
| 76 22 4 17 13 49 4 18 32 40 96 57 77 20 1 52
|
|
| 32 22 4 17 13 20 1 18 76 40 96 57 77 49 4 52
|
|
| 32 22 4 17 13 20 1 18 76 40 96 57 77 49 4 52
|
|
| 13 20 1 17 32 22 4 18 76 40 4 52 77 49 96 57
|
|
| 13 20 1 17 32 22 4 18 76 40 4 52 77 49 96 57
|
|
| 1 17 13 20 4 18 32 22 4 40 76 49 77 52 96 57
|
|
| 1 17 4 18 13 20 4 22 32 40 76 49 77 52 96 57
|
|
| 1 17 4 18 4 20 13 22 32 40 76 49 77 52 96 57
|
|
| 1 17 4 18 4 20 13 22 32 40 76 49 77 52 96 57
|
|
| 1 4 17 4 18 13 20 22 32 40 49 76 52 77 57 96
|
|
| 1 4 4 17 13 18 20 22 32 40 49 52 76 57 77 96
|
|
| 1 4 4 13 17 18 20 22 32 40 49 52 57 76 77 96
|
|
|
1 4 4 13 17 18 20 22 32 40 49 52 57 76 77 96
| результат
|
| 1 4 4 13 17 18 20 22 32 40 49 52 57 76 77 96
|
Сортировки включением
СОРТИРОВКА ПРОСТЫМИ ВСТАВКАМИ. Этот метод - "дословная" реализация стратегии включения. Порядок алгоритма сортировки простыми вставками - O(N^2), если учитывать только операции сравнения.
Но сортировка требует еще и в среднем N2./4 перемещений, что делает ее в таком варианте значительно менее эффективной, чем сортировка выборкой.
Алгоритм сортировки простыми вставками иллюстрируется программным примером 3.11.
{===== Программный пример 3.11 =====}
Procedure Sort(a : Seq; var b : Seq);
Var i, j, k : integer;
begin
for i:=1 to N do { перебор входного массива }
{ поиск места для a[i] в выходном массиве }
begin j:=1; while (j<i) and (b[j]<=a[i]) do j:=j+1;
{ освобождение места для нового эл-та}
for k:=i downto j+1 do b[k]:=b[k-1];
b[j]:=a[i]; { запись в выходной массив }
end; end;
Эффективность алгоритма может быть несколько улучшена при применении не линейного, а дихотомического поиска. Однако, следует иметь в виду, что такое увеличение эффективности может быть достигнуто только на множествах значительного по количеству элементов объема. Но поскольку алгоритм требует большого числа пересылок, то при значительном объеме одной записи эффективность может определяться не количеством операций сравнения, а количеством пересылок. Алгоритм обменной сортировки простыми вставками отличается от базового алгоритма только тем, что входное и выходное множества будут разделять одну область памяти.
ПУЗЫРЬКОВАЯ СОРТИРОВКА ВСТАВКАМИ. Это модификация обменного варианта сортировки. При такой сортировке входное и выходное множества находятся в одной последовательности, причем выходное - в начальной ее части. В исходном состоянии можно считать, что первый элемент последовательности уже принадлежит упорядоченному выходному множеству, остальная часть последовательности - неупорядоченное входное. Первый элемент входного множества примыкает к концу выходного множества. На каждом шаге сортировки происходит перераспределение последовательности: выходное множество увеличивается на один элемент, а входное - уменьшается. Это происходит за счет того, что первый элемент входного множества теперь считается последним элементом выходного. Затем выполняется просмотр выходного множества от конца к началу с перестановкой соседних элементов, которые не соответствуют критерию упорядоченности.
Просмотр прекращается, когда прекращаются перестановки. Это приводит к тому, что последний элемент выходного множества "выплывает" на свое место в множестве. Поскольку при этом перестановка приводит к сдвигу нового в выходном множестве элемента на одну позицию влево, нет смысла всякий раз производить полный обмен между соседними элементами - достаточно сдвигать старый элемент вправо, а новый элемент записать в выходное множество, когда его место будет установлено. Именно так и построен программный пример пузырьковой сортировки вставками - 3.12.
{===== Программный пример 3.12 =====}
Procedure Sort (var a : Seq);
Var i, j, k, t : integer;
begin for i:=2 to N do { перебор входного массива }
{*** вх.множество - [i..N], вых.множество - [1..i] }
begin t:=a[i]; { запоминается значение нового эл-та }
j:=i-1; {поиск места для эл. в вых. множестве со сдвигом }
{конец цикла при достижении начала или, если найден эл.меньший}
нового} while (j>=1) and (a[j]>t) do
begin a[j+1]:=a[j]; { все эл-ты, большие нового, сдвигаются}
j:=j-1; { цикл от конца к началу выходного множества }
end; a[j+1]:=t; { новый эл-т ставится на свое место }
end; end;
Результаты трассировки программного примера 3.12 представлены в табл. 3.8.
Таблица 3.8
Шаг
| Содержимое массива а
| исходный
| 48:43 90 39 9 56 40 41 75 72
|
| 43 48:90 39 9 56 40 41 75 72
|
| 43 48 90:39 9 56 40 41 75 72
|
| 39 43 48 90: 9 56 40 41 75 72
|
| 9 39 43 48 90:56 40 41 75 72
|
| 9 39 43 48 56 90:40 41 75 72
|
| 9 39 40 43 48 56 90:41 75 72
|
| 9 39 40 41 43 48 56 90:75 72
|
| 9 39 40 41 43 48 56 75 90:72
| результат
| 9 39 40 41 43 48 56 72 75 90:
|
Хотя обменные алгоритмы стратегии включения и позволяют сократить число сравнений при наличии некоторой исходной упорядоченности входного множества, значительное число пересылок существенно снижает эффективность этих алгоритмов. Поэтому алгоритмы включения целесообразно применять к связным структурам данных, когда операция перестановки элементов структуры требует не пересылки данных в памяти, а выполняется способом коррекции указателей (см. главу 5).
Еще одна группа включающих алгоритмов сортировки использует структуру дерева. Рассмотрение последующих алгоритмов будет полезно после ознакомления с главой 6.
СОРТИРОВКА УПОРЯДОЧЕННЫМ ДВОИЧНЫМ ДЕРЕВОМ. Алгоритм складывается из построения упорядоченного двоичного дерева и последующего его обхода. Если нет необходимости в построении всего линейного упорядоченного списка значений, то нет необходимости и в обходе дерева, в этом случае применяется поиск в упорядоченном двоичном дереве. Алгоритмы работы с упорядоченными двоичными деревьями подробно рассмотрены в главе 6. Отметим, что порядок алгоритма - O(N*log2N)), но в конкретных случаях все зависит от упорядоченности исходной последовательности, которая влияет на степень сбалансированности дерева и в конечном счете - на эффективность поиска.
ТУРНИРНАЯ СОРТИРОВКА. Этот метод сортировки получил свое название из-за сходства с кубковой системой проведения спортивных соревнований: участники соревнований разбиваются на пары, в которых разыгрывается первый тур; из победителей первого тура составляются пары для розыгрыша второго тура и т.д. Алгоритм сортировки состоит из двух этапов. На первом этапе строится дерево, аналогичное схеме розыгрыша кубка.
Например: для последовательности чисел: 16 21 8 14 26 94 30 1 такое дерево будет иметь вид пирамиды, показанной на рис. 3.13.
Рис.3.13. Пирамида турнирной сортировки
В примере 3.13 приведена программная иллюстрация алгоритма турнирной сортировки. Она нуждается в некоторых пояснениях. Построение пирамиды выполняется функцией Create_Heap. Пирамида строится от основания к вершине. Элементы, составляющие каждый уровень, связываются в линейный список, поэтому каждый узел дерева помимо обычных указателей на потомков - left и right, содержит и указатель на "брата" - next. При работе с каждым уровнем указатель содержит начальный адрес списка элементов в данном уровне. В первой фазе строится линейный список для нижнего уровня пирамиды, в элементы которого заносятся ключи из исходной последовательности. Следующий цикл while в каждой своей итерации надстраивает следующий уровень пирамиды. Условием завершения этого цикла является получение списка, состоящего из единственного элемента, то есть вершины пирамиды. Построение очередного уровня состоит в попарном переборе элементов списка, составляющего предыдущий (нижний) уровень. В новый уровень переносится наименьшее значение ключа из каждой пары.
Следующий этап состоит в выборке значений из пирамиды и формирования из них упорядоченной последовательности (процедура Heap_Sort и функция Competit). В каждой итерации цикла процедуры Heap_Sort выбирается значение из вершины пирамиды - это наименьшее из имеющихся в пирамиде значений ключа. Узел-вершина при этом освобождается, освобождаются также и все узлы, занимаемые выбранным значением на более низких уровнях пирамиды. За освободившиеся узлы устраивается (снизу вверх) состязание между их потомками.
Так, для пирамиды, исходное состояние которой было показано на рис 3.13, при выборке первых трех ключей (1, 8, 14) пирамида будет последовательно принимать вид, показанный на рис.3.14.
Рис.3.14. Пирамида после последовательных выборок
Процедура Heap_Sort в программном примере 3.13 получает входной параметр ph - указатель на вершину пирамиды и формирует выходной параметр a - упорядоченный массив чисел. Вся процедура Heap_Sort состоит из цикла, в каждой итерации которого значение из вершины переносится в массив a, а затем вызывается функция Competit, которая обеспечивает реорганизацию пирамиды в связи с удалением значения из вершины.
Функция Competet рекурсивная, ее параметром является указатель на вершину того поддерева, которое подлежит реорганизации. В первой фазе функции устанавливается, есть ли у узла, составляющего вершину заданного поддерева, потомок, значение данных в котором совпадает со значением данных в вершине. Если такой потомок есть, то функция Competit вызывает сама себя для реорганизации того поддерева, вершиной которого является обнаруженный потомок. После реорганизации адрес потомка в узле заменяется тем адресом, который вернул рекурсивный вызов Competit. Если после реорганизации оказывается, что у узла нет потомков (или он не имел потомков с самого начала), то узел уничтожается и функция возвращает пустой указатель. Если же у узла еще остаются потомки, то в поле данных узла заносится значение данных из того потомка, в котором это значение наименьшее, и функция возвращает прежний адрес узла.
{===== Программный пример 3.13 =====}
{ Турнирная сортировка }
type nptr = ^node; { указатель на узел }
node = record { узел дерева }
key : integer; { данные }
left, right : nptr; { указатели на потомков }
next : nptr; { указатель на "брата" }
end;
{ Создание дерева - функция возвращает указатель на вершину
созданного дерева }
Function Heap_Create(a : Seq) : nptr;
var i : integer;
ph2 : nptr; { адрес начала списка уровня }
p1 : nptr; { адрес нового элемента }
p2 : nptr; { адрес предыдущего элемента }
pp1, pp2 : nptr; { адреса соревнующейся пары }
begin
ph2:=nil; {Фаза 1- построение самого нижнего уровня пирамиды}
for i:=1 to n do
begin New(p1); { выделение памяти для нового эл-та }
p1^.key:=a[i]; { запись данных из массива }
p1^.left:=nil; p1^.right:=nil; { потомков нет }
{ связывание в линейный список по уровню }
if ph2=nil then ph2:=p1 else p2^.next:=p1; p2:=p1;
end; { for } p1^.next:=nil;
{ Фаза 2 - построение других уровней }
while ph2^.next<>nil do { цикл до вершины пирамиды }
begin pp1:=ph2; ph2:=nil; { начало нового уровня }
while pp1<>nil do { цикл по очередному уровню }
begin pp2:=pp1^.next; New(p1);
{ адреса потомков из предыдущего уровня }
p1^.left:=pp1; p1^.right:=pp2; p1^.next:=nil;
{ связывание в линейный список по уровню }
if ph2=nil then ph2:=p1 else p2^.next:=p1; p2:=p1;
{ состязание данных за выход на уровень }
if (pp2=nil)or(pp2^.key>pp1^.key) then p1^.key:=pp1^.key
else p1^.key:=pp2^.key; { переход к следующей паре }
if pp2<>nil then pp1:=pp2^.next else pp1:=nil;
end; { while pp1<>nil }
end; { while ph2^.next<>nil }
Heap_Create:=ph2; end;
{ Реорганизация поддерева - функция возвращает указатель на вершину
реорганизованного дерева }
Function Competit(ph : nptr) : nptr;
begin
{ определение наличия потомков, выбор потомка для реорганизации,
реорганизация его }
if (ph^.left<>nil)and(ph^.left^.key=ph^.key) then
ph^.left:=Competit(ph^.left)
else if (ph^.right<>nil) then
ph^.right:=Competit(ph^.right);
if (ph^.left=nil)and(ph^.right=nil) then
{ освобождение пустого узла }
begin Dispose(ph); ph:=nil; end;
else { состязание данных потомков }
if (ph^.left=nil) or
((ph^.right<>nil)and(ph^.left^.key>ph^.right^.key)) then
ph^.key:=ph^.right^.key
else ph^.key:=ph^.left^.key;
Competit:=ph; end;
Procedure Heap_Sort(var a : Seq); { Сортировка }
var ph : nptr; { адрес вершины дерева }
i : integer;
begin
ph:=Heap_Create(a); { создание дерева }
for i:=1 to N do { выборка из дерева }
begin a[i]:=ph^.key; { перенос данных из вершины в массив }
ph:=Competit(ph); { реорганизация дерева }
end; end;
Построение дерева требует N-1 сравнений, выборка - N*log2(N) сравнений. Порядок алгоритма, таким образом, O(N*log2(N)). Сложность операций над связными структурами данных, однако, значительно выше, чем над статическими структурами. Кроме того, алгоритм неэкономичен в отношении памяти: дублирование данных на разных уровнях пирамиды приводит к тому, что рабочая область памяти содержит примерно 2*N узлов.
СОРТИРОВКА ЧАСТИЧНО УПОРЯДОЧЕННЫМ ДЕРЕВОМ. В двоичном дереве, которое строится в этом методе сортировки, для каждого узла справедливо следующее утверждение: значения ключа, записанное в узле, меньше, чем ключи его потомков. Для полностью упорядоченного дерева имеются требования к соотношению между ключами потомков. Для данного дерева таких требований нет, поэтому такое дерево и называется частично упорядоченным. Кроме того, дерево должно быть абсолютно сбалансированным. Это означает не только то, что длины путей к любым двум листьям различаются не более чем на 1, но и то, что при добавлении нового элемента в дерево предпочтение всегда отдается левой ветви/подветви, пока это не нарушает сбалансированность. Более подробно деревья рассматриваются в гл.6.
Например: последовательность чисел: 3 20 12 58 35 30 32 28 будет представлена в виде дерева, показанного на рис. 3.15.
Рис.3.15. Частично упорядоченное дерево
Представление дерева в виде пирамиды наглядно показывает, что для такого дерева можно ввести понятия "начала" и "конца". Началом, естественно, будет считаться вершина пирамиды, а концом - крайний левый элемент в самом нижнем ряду (на рис.3.15 это 58).
Для сортировки этим методом должны быть определены две операции: вставка в дерево нового элемента и выборка из дерева минимального элемента; причем выполнение любой из этих операций не должно нарушать ни сформулированной выше частичной упорядоченности дерева, ни его сбалансированности.
Алгоритм вставки состоит в следующем. Новый элемент вставляется на первое свободное место за концом дерева (на рис.3.15 это место обозначено символом "*"). Если ключ вставленного элемента меньше, чем ключ его предка, то предок и вставленный элемент меняются местами. Ключ вставленного элемента теперь сравнивается с ключом его предка на новом месте, и т.д. Сравнения заканчиваются, когда ключ нового элемента окажется больше ключа предка или когда новый элемент "выплывет" в вершину пирамиды. Пирамида, показанная на рис.3.15, построена именно последовательным включением в нее чисел из приведенного ряда. Если мы включим в нее, например, еще число 16, то пирамида примет вид, представленный на рис.3.16. (Символом "*" помечены элементы, перемещенные при этой операции.)
Процедура выборки элемента несколько сложнее. Очевидно, что минимальный элемент находится в вершине. После выборки за освободившееся место устраивается состязание между потомками, и в вершину перемещается потомок с наименьшим значением ключа. За освободившееся место перемешенного потомка состязаются его потомки и т.д., пока свободное место не опустится до листа пирамиды. Состояние дерева после выборки из него минимального числа (3) показано на рис.3.17,а.
Рис.3.16. Частично упорядоченное дерево, включение элемента
Рис.3.17. Частично упорядоченное дерево, исключение элемента
Упорядоченность дерева восстановлена, но нарушено условие его сбалансированности, так как свободное место находится не в конце дерева. Для восстановления сбалансированности последний элемент дерева переносится на освободившееся место, а затем "всплывает" по тому же алгоритму, который применялся при вставке. Результат такой балансировки показан на рис.3.17,б.
Прежде чем описывать программный пример, иллюстрирующий сортировку частично упорядоченным деревом - пример 3.14, рассмотрим способ представления дерева в памяти. Это способ представления двоичных деревьев в статической памяти (в одномерном массиве), который может быть применен и в других задачах. Элементы дерева располагаются в соседних слотах памяти по уровням. Самый первый слот выделенной памяти занимает вершина. Следующие 2 слота - элементы второго уровня, следующие 4 слота - третьего и т.д.
Дерево, изображенное на рис.3.17,б, например, будет линеаризовано таким образом:
12 16 28 20 35 30 32 58
В таком представлении отпадает необходимость хранить в составе узла дерева указатели, так как адреса потомков могут быть вычислены. Для узла, представленного элементом массива с индексом i, индексы его левого и правого потомков будут 2*i и 2*i+1 соответственно. Для узла с индексом i индекс его предка будет i div 2.
После всего вышесказанного алгоритм программного примера 3.14 не нуждается в особых пояснениях. Поясним только структуру примера. Пример оформлен в виде законченного программного модуля, который будет использован и в следующем примере. Само дерево представлено в массиве tree, переменная nt является индексом первого свободного элемента в массиве. Входные точки модуля:
· процедура InitST - инициализация модуля, установка начального значения nt;
· функция InsertST - вставка в дерево нового элемента; функция возвращает false, если в дереве нет свободного места, иначе - true;
· функция DeleteST - выборка из дерева минимального элемента;
· функция возвращает false, если дерево пустое, иначе - true;
· функция CheckST - проверка состояния дерева: ключ минимального элемента возвращается в выходном параметре, но элемент не исключается из дерева; а возвращаемое значение функции - 0 – если дерево пустое, 1 - если дерево заполнено не до конца, 2 – если дерево заполнено до конца.
Кроме того, в модуле определены внутренние программные единицы:
· функция Down - обеспечивает спуск свободного места из вершины пирамиды в ее основание, функция возвращает индекс свободного места после спуска;
· процедура Up - обеспечивает всплытие элемента с заданного места.
{===== Программный пример 3.14 =====}
Unit SortTree; { Сортировка частично упорядоченным деревом }
Interface
Procedure InitSt;
Function CheckST(var a : integer) : integer;
Function DeleteST(var a : integer) : boolean;
Function InsertST(a : integer) : boolean;
Implementation
Const NN=16;
var tr : array[1..NN] of integer; { дерево }
nt : integer; { индек последнего эл-та в дереве }
Procedure Up(l : integer); {Всплытие эл-та с места с индексом l }
var h : integer; {l - индекс узла, h - индекс его предка }
x : integer;
begin h:=l div 2; { индекс предка }
while h>0 do { до начала дерева }
if tr[l]<tr[h] then { ключ узла меньше, чем у предка }
begin x:=tr[l]; tr[l]:=tr[h]; tr[h]:=x; { перестановка }
l:=h; h:=l div 2; { предок становится текущим узлом }
end else h:=0; { конец всплытия }
end; { Procedure Up }
{** Спуск свободного места из начала дерева **}
Function Down : integer;
var h, l : integer; { h - индекс узла, l - индекс его потомка }
begin h:=1; { начальный узел - начало дерева }
while true do
begin l:=h*2; { вычисление индекса 1-го потомка }
if l+1<=nt then { у узла есть 2-й потомок }
begin if tr[l]<=tr[l+1] then { 1-й потомок меньше 2-го }
begin tr[h]:=tr[l]; {1-й потомок переносится в тек. узел }
h:=l; end { 1-й потомок становится текущим узлом }
else { 2-й потомок меньше 1-го }
begin tr[h]:=tr[l+1]; {2-й потомок переносится в текущ.узел }
h:=l+1; end; {2-й потомок становится текущим узлом }
end else
if l=nt then { 1-й потомок есть, 2-го нет }
begin tr[h]:=tr[l]; {1-й потомок переносится в текущ.узел }
Down:=l; Exit; { спуск закончен }
end else { потомков нет - спуск закончен }
begin Down:=h; Exit; end;
end; { while }
end; { Function Down }
Procedure InitSt; {** Инициализация сортировки деревом **}
begin nt:=0; { дерево пустое }
end; { Procedure InitSt }
{** Проверка состояния дерева **}
Function CheckST(var a : integer) : integer;
begin a:=tr[1]; { выборка эл-та из начала }
case nt of { формирование возвращаемого значения функции }
0: { дерево пустое } CheckSt:=0;
NN: { дерево полное } CheckSt:=2;
else { дерево частично заполнено } CheckSt:=1;
end;
end; { Function CheckST }
{** Вставка эл-та a в дерево **}
Function InsertST(a : integer) : boolean;
begin
if nt=NN then { дерево заполнено - отказ }
InsertST:=false else { в дереве есть место }
begin nt:=nt+1; tr[nt]:=a; { запись в конец дерева }
Up(nt); InsertSt:=true; { всплытие }
end; end; { Function InsertST }
{** Выборка эл-та из дерева **}
Function DeleteST(var a : integer) : boolean;
var n : integer;
begin
if nt=0 then { дерево пустое - отказ }
DeleteST:=false else { дерево не пустое }
begin a:=tr[1]; { выборка эл-та из начала }
n:=Down; { спуск свободного места в позицию n }
if n<nt then begin
{ если свободное место спустилось не в конец дерева }
tr[n]:=tr[nt]; { эл-т из конца переносится на своб.место }
Up(n); end; { всплытие }
nt:=nt-1; DeleteSt:=true;
end; end; { Function DeleteST }
END.
Не нашли, что искали? Воспользуйтесь поиском по сайту:
©2015 - 2024 stydopedia.ru Все материалы защищены законодательством РФ.
|