
- •1Введение
- •2Сортировки
- •2.1Сортировки массивов
- •2.2Сортировка простым включением
- •2.3Сортировка простым выбором
- •2.4Сортировка простым обменом
- •2.5Сравнение простых сортировок
- •2.6 Сортировка шелла
- •2.7Пирамидальная сортировка
- •2.8Быстрая сортировка
- •2.9Поиск медианы и квантилей
- •2.10Сравнение сортировок
- •3Поиск подстроки в строке
- •3.1Поиск в строке
- •3.2Простой поиск в строке
- •3.3Поиск в строке. Алгоритм боуера-мура
- •4Генерация перестановок
- •4.1Генерация перестановок последовательности натуральных чисел
- •4.2Генерация перестановок элементов в антилексикографическом порядке
- •4.3Генерация перестановок за одну транспозицию элементов
- •4.4Генерация перестановок за одну транспозицию соседних элементов
- •5Генерация подмножеств
- •5.1Генерация всех подмножеств множества
- •5.2Генерация m -элементных подмножеств множества натуральных чисел
- •Var m: integer; {Размер подмножества}
- •Var I, j, p: integer;
- •5.3Генрация k-компонентных выборок на множестве {0, 1}
- •Var k: integer; {Количество нулей в кортеже}
- •I: integer;
- •Var I, j, p: integer;
- •If Finish then Break {Exit};
- •6Генерация разбиений
- •6.1Разбиение целых чисел
- •Var I, j: integer;
- •Var j: integer;
- •Var I, j, k: integer;
- •Var d, l, Sum: integer;
- •6.2Разбиение множеств
- •/1, 2, 3/ И /4/ затем /1, 2/ и /3, 4/ и т.Д. }
- •I, j, k, r, s: integer;
- •If not Flag2 then
- •If Flag1 then
- •If Forvd[j] then { j движется вперед}
- •7Обходы бинарных деревьев
- •7.1Процедуры прохождения бинарных деревьев
- •8Поиск на бинарных деревьях
- •8.1Процедуры поиска на бинарных деревьях
- •Рекомендованная литература
2.8Быстрая сортировка
Быстрая сортировка или сортировка с разделением является улучшением сортировки методом “пузырька”.
Оказалось, что усовершенствование самой непроизводительной сортировки, основанной на обмене, дало самые лучшие на сегодня результаты. Эта сортировка была предложена К. Хоором в 1962 г., а затем - в улучшенном варианте, в 1971 г.
Быстрая сортировка основана на упоминаемом ранее факте, что для достижения наибольшей эффективности желательно производить обмены элементов на больших расстояниях. Поясним идею быстрой сортировки на наиболее ярком примере.
Пусть дан массив из n элементов с ключами, расположенными в обратном порядке. Этот массив можно отсортировать, выполнив всего n/2 обменов, если поменять сначала самый левый и самый правый элементы. Затем среди оставшихся элементов проделаем те же действия и т.д., постепенно продвигаясь с двух концов массива к средине.
Ниже приведен пример такой сортировки:
-
9
9
77
55
44
33
22
11
0
1
0
1
7
7
55
44
33
22
11
99
и т.д.
В результате получим
01
11
22
33
44
55
77
99
Приведенный пример приводит к определённым предложениям.
Рассмотрим следующий алгоритм:
Выбирается случайным образом, какой то элемент и обозначается как x.
Выполняется просмотр массива слева (от начала) пока не встретится элемент ai > х.
Выполняется просмотр массива справа (от конца) пока не встретится элемент aj < х.
Меняются местами элементы ai и aj
Продолжается процесс просмотра с обменом, пока оба просмотра не встретятся где-то посреди массива.
В результате массив разделится на две части: левую - с ключами меньшими, чем x, и правую - с ключами большими, чем x.
Заметим, что отношения ai > x и aj < x можно заменить на отношения ai >= x и aj <= x. , что позволяет обрабатывать дубликаты. Отрицанием полученных отношений являются отношения ai < x и aj > x. Применение отношений ai < x и aj > x в соответствующих операторах цикла while позволяет рассматривать элемент x как “барьер” для обоих просмотров.
Теперь реализуем процесс разбиения массива на части в виде процедуры Partition:
procedure Partition;
var W, X : Item;
I, J : integer;
N: integer; {количество элементов сортируемой последовательности}
begin
N := Length (A);
I := 0;
J := N-1;
{случайно выбирается X}
repeat
{просмотр массива слева}
while A[I].Key < X.Key do I := I+1;
{просмотр массива справа}
while A[J].Key > X.Key do J := J-1;
if I <= J then
begin {перестановка элементов}
W := A[I];
A[I] := A[J];
A[J] := W;
I := I+1;
J := J-1;
end;
until I > J;
end; {Partition}
Вернемся к уже используемому ранее массиву:
-
44
55
11
33
99
01
22
77
Пусть случайно будет выбран элемент “33”.
Для того чтобы разделить массив относительно элемента “33”, потребуется два обмена
-
22
01
11
33
99
55
44
77
При этом:
Согласно рассматриваемой процедуре Partition конечными значениями индексов являются i = 5 и j = 3 (алгоритм останавливается при i > j).
Ключи элементов массива от a1 до ai-1 меньше или равны “33”.
Ключи элементов массива от aj+1 до an больше или равны “33”.
Следовательно, получены два не отсортированных подмассива, таких что
ak.key <= x.key k = 1, 2,..., i-1
и
ak.key >= x.key k = j+1,..., n
и группу элементов, совпадающих с барьером,
ak.key = x.key k = i,..., j
Эта группа состоит из одного элемента, в случае отсутствия дубликатов элемента x, и из нескольких элементов при наличии дубликатов элемента x.
В приведенной выше последовательности количество элементов, больших барьера и расположенных слева от барьера, совпадает с количеством элементов, меньших барьера и расположенных справа от барьера, что можно рассматривать как частный случай. Для демонстрации результативности алгоритма в общем случае рассмотрим другой массив
-
44
55
11
33
05
01
22
77
Пусть по прежнему будет выбран элемент “33”. Теперь слева от элемента "33" только два элемента, больших "33", а справа – три элемента меньших "33". Следовательно, возникает асимметрия в размерах групп элементов, расположенных не на своем месте. Для того чтобы разделить массив относительно элемента “33”, потребуется количество обменов, равное размеру большей из групп, т.е. три обмена.
-
22
01
11
05
33
55
44
77
Согласно процедуре Partition конечными значениями индексов являются i = 5 и j = 4 (алгоритм останавливается при i > j), а барьер - элемент "33", перемещается на другую позицию.
Из приведенного примера видно, что результаты разбиения не зависят от первоначального местоположения "барьера" в исходной последовательности, а зависят только от значения "барьера" и от значений других элементов массива. По мере необходимости алгоритм (группа операторов перестановки) перемещает "барьер" вправо или влево по массиву и таких перемещений может быть несколько.
Алгоритм разбиения массива на части очень быстрый, но на отдельных сочетаниях элементов может быть не очень эффективен и для своего усовершенствования требует некоторых усложнений. Однако на фоне всех остальных сочетаний элементов “тяжёлые” сочетания встречаются крайне редко и, поэтому нет смысла усложнять алгоритм.
Дальнейшая идея сортировки довольно проста - с каждым из полученных двух подмассивов проделать то же, что и с исходным массивом и т.д. до тех пор, пока каждая из частей не будет состоять из одного элемента. Эти действия достигаются за счёт применения рекурсии и описываются следующей программой:
type TArr = array of Item ;
var I: integer;
N: integer; {количество элементов сортируемой последовательности}
procedure QuickSort (var A: TArr);
procedure Sort (L, R: integer); {обобщение процедуры Partition для плавающих границ массива}
var I, J : integer;
W, X : Item;
begin
I := L;
J := R;
{определяется элемент Х, который будет выступать в качестве барьера. Барьер выбирается посреди обрабатываемой последовательности}
X := A[(L+R) div 2]; { X := A[(R-L) div 2 + L]}
repeat {Просмотр массива}
while A[I].Key < X.Key do I := I + 1;
while X.Key < A[J].Key do J := J - 1;
if I <= J then
begin
W := A[I];
A[I] := A[J];
A[J] := W;
I := I + 1;
J := J - 1;
end;
until I > J;
if L < J then Sort(L, J); {Обрабатывается левый подмассив}
if I < R then Sort(I, R); {Обрабатывается правый подмассив}
end; {Sort}
begin
N := Length (A);
Sort(0, N-1);
end; {QuickSort}
begin
{формирование и заполнение массива А}
QuickSort (A); {сортировка массива}
···
end.
Рассмотренный алгоритм быстрой сортировки имеет рекурсивный характер. На больших размерах массива это может приводить к определенному замедлению алгоритма, за счет существенных затрат на обработку системного стека программы таких, как сохранение адреса возврата в вызывающую процедуру, а также всех параметров, передаваемых по значению, и всех локальных переменных процедуры.
Уменьшить эти затраты можно, если рекурсию заменить итерацией, ввести в программу собственный стек для хранения информации о требуемых разбиениях массива и действия по управлению стеком.
На каждом этапе работы алгоритма сортируемая последовательность разбивается на две части (подпоследовательности). Но, согласно алгоритма, только одна из этих частей – левая, может сразу подвергаться последующей итерации, а информация о границах другой части должна быть занесена в стек. Когда обработка левой части соответствующей последовательности завершается, из верхушки стека извлекается нужная информация и выполняется итеративная обработка правой части этой последовательности.
Ниже приводится нерекурсивный вариант процедуры быстрой сортировки NotRecQuickSort. В этой процедуре границы обрабатываемой части последовательности задаются ее левым и правым индексами. Таким образом, стек QStack может быть представлен двумерным массивом пар индексов. Верхушка стека будет обозначаться как TopQStack.
procedure NotRecQuickSort (var A: TArr); {нерекурсивный вариант процедуры QuickSort}
var I, J, L, R: integer;
W, X: Item;
QStack: array of {стек – динамический массив записей из двух полей}
record
L, R: integer;
end;
TopQStack: integer; {верхушка стека}
N: integer; {размер массива}
begin
N := Length (A);
SetLength (QStack, Ln(N) div Ln(2));} {размер стека равен Log2N}
{начальное заполнение стека границами массива L=0 и R=N-1, т.к. алгоритм построен на выборе границ обрабатываемого массива из стека}
TopQStack := 0;
QStack[1].L := 0;
QStack[1].R := N-1;
repeat {выбор из стека последнего запроса}
L := QStack[TopQStack].L;
R := QStack[TopQStack].R;
TopQStack := TopQStack – 1;
repeat {разделение последовательности на левую и правую часть}
I := L;
J := R;
{определяется элемент Х, который будет выступать в качестве барьера. Барьер выбирается посреди обрабатываемой последовательности}
X := A[(L+R) div 2];
repeat {Просмотр массива}
while A[I].Key < X.Key do I := I + 1; {поиск несоответствия слева}
while X.Key < A[J].Key do J := J - 1; {поиск несоответствия справа}
if I <= J then
begin
W := A[I];
A[I] := A[J];
A[J] := W;
I := I + 1;
J := J - 1;
end;
until I > J;
if L < J then {запись в стек границ правой части последовательности}
begin
TopQStack := TopQStack + 1;
QStack[TopQStack].L := I;
QStack[TopQStack].R := R;
end;
R := J; {теперь L и R ограничивают левую часть}
until L >= R;
until TopQStack = 0;
end; {NotRecQuickSort}
В этом алгоритме дополнительного рассмотрения требует вопрос о размере стека. В зависимости от способа выбора барьера в сортируемой последовательности он колеблется от N до Log2N.
Худший случай имеет место, если элемент для барьера выбирается, например случайным образом, так, что в правой части всех обрабатываемых последовательностей содержится только один элемент. Тогда в стек будет заноситься информация о N правых частях.
Лучший случай имеет место, если элемент для барьера выбирается посреди обрабатываемой последовательности, что и реализовано в приведенном алгоритме.
В общем случае, можно предложить выбирать начальный размер стека, реализуемого динамическим массивом, равным Log2N, а затем, по мере необходимости, увеличивать его определенными порциями.
Основной недостаток быстрой сортировки, также как и других усовершенствованных методов сортировки, - низкая производительность при небольших размерах сортируемых последовательностей. Сама же логика быстрой сортировки приводит к тому, что размер обрабатываемых последовательностей постоянно сокращается. Но, в отличие от других сортировок, быстрая сортировка для сортировки коротких последовательностей позволяет включить какой либо метод простой сортировки, например StraightSelection, модифицированный для работы с границами массива.
Для этого необходимо несколько модифицировать операторы перехода к сортировке следующей части последовательности. Например, для рекурсивного варианта этой сортировки получим:
if L < J then {Обрабатывается левый подмассив}
if J-L > 15 then Sort(L, J);
else StraightSelection (L, J);
if I < R then {Обрабатывается правый подмассив}
if R-I > 15 then Sort(I, R);
else StraightSelection (I, R);
В приведенных вариантах алгоритма быстрой сортировки для разделения последовательности сортируемых элементов выбирается элемент, расположенный посреди последовательности (подпоследовательности). На самом деле как показано выше при анализе процедуры Partition, можно выбрать элемент, расположенный на произвольной позиции. Алгоритм сортировки от этого не меняется, но может изменяться его производительность. В наилучшем случае, когда для разделения последовательности удаётся выбрать элемент со значением (но не позицией), которое разбивает последовательность на две подпоследовательности одинакового размера. В приведенном ранее примере это элемент “33”.
-
44
55
11
33
99
01
22
77
Такой элемент называется медианой (см. далее). В случае выбора медианы для разделения последовательности сортируемых элементов, общее число сравнений равно log2n, а общее число обменов (n/6)*log2(n).
Вероятность выбора медианы в последовательности из n элементов равна 1/n. Поэтому, в общем случае, эффективность быстрой сортировки ниже оптимальной. Однако эти отличия незначительны и при выборе в качестве барьера любого элемента массива эффективность сортировки в среднем только в 2*ln2 1.38 раза хуже оптимальной.
Можно существенно приблизить эффективность быстрой сортировки к оптимальному значению за счет введения специальных средств поиска медианы и их использования при поиске очередного барьера Х в приведенном выше алгоритме QuickSort.