Реализация
procedure QuickSort;
procedure sort (l,r: index);
var i,j: index; w,x: item;
begin 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 sort(l,j);
if (i<r) then sort(i,r)
end;
begin sort(1,n);
end; { QuickSort }
Процедура sort рекурсивно вызывает сама себя. Такое использование рекурсии в алгоритмах - очень мощное средство. В некоторых языках программирования старого образца рекурсия по некоторым техническим причинам запрещена. Поэтому выразим тот же алгоритм в виде не рекурсивной процедуры. Очевидно, что при этом рекурсия представляется как итерация, причем необходимы некоторые операции для хранения информации.
Основа итеративного решения - введение списка вопросов на разделения, которые еще предстоит выполнить. После каждого шага нужно произвести два очередных разделения, и лишь одно из них можно выполнить непосредственно при следующей итерации, запрос на другое заносится в список. Важно, разумеется, что вопросы из списка выполняются в обратном порядке. Это предполагает, что первый занесенный в список запрос выполняется последним и наоборот; список ведет себя как пульсирующий стек. В следующей не рекурсивной версии быстрой сортировки каждый запрос представлен просто левым и правыми индексами, определяющими границы части, которую впоследствии нужно будет разделить. Итак, мы вводим переменную - массив, называемую stack, и индекс s, указывающий на самую последнюю запись в этом стеке. Вопрос о размере стека m требует более детального анализа алгоритма.
Реализация
procedure QuickSort1;
const m = 12;
var i,j,l,r: index;
x,w: item;
s: 0..m;
stack: array[1..m] of
record l,r: index end;
begin s:= 1; stack[1].l:=1; stack[1].r:=n;
repeat {выбор запроса из вершины стека}
l:=stack[s].l; r:=stack[s].r; s:=s-1;
repeat { разделить A[l] ... A[r] }
i:=l; j:=r; x:=A[(l+r) div2];
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 (i<r) then
begin { запись в стек запроса на сортировку правой части }
s:=s+1; stack[s].l:=i; stack[s].r:=r;
end;
r:=j;
until (l>=r);
until (s=0);
end; { QuickSort1 }
Словари - это структуры данных, которые поддерживают операции поиска, вставки и удаления. Один из наиболее эффективных способов реализации словаря - хеш-таблицы. Как правило, к ключам применяется какая-нибудь простая функция, значения которой дают место ключа в словаре; хорошая функция рандомизирует значения ключей, рассеивая их по интервалу [1, N], где N - количество мест в словаре (объем хеш-таблицы). Мы рассмотрим также двоичные и красно-черные деревья. Методы работы с обоими типами деревьев включают приемы, с которыми мы познакомились при изучении бинарного поиска. Наконец, слоёные списки (skip lists) - еще одна иллюстрация пользы применения случайных чисел при построении словарей.
Один из наиболее эффективных способов реализации словаря - хеш-таблица. Среднее время поиска элемента в ней есть O(1), время для наихудшего случая - O(n). Прекрасное изложение хеширования можно найти в работах Кормена [1990] [Cormen] и Кнута [1998]
Хеш-таблица - это обычный массив с необычной адресацией, задаваемой хеш-функцией. Например, на рис. 3.1 hashTable - это массив из 8 элементов. Каждый элемент представляет собой указатель на линейный список, хранящий числа. Хеш-функция в этом примере просто делит ключ на 8 и использует остаток как индекс в таблице. Это дает нам числа от 0 до 7 Поскольку для адресации в hashTable нам и нужны числа от 0 до 7, алгоритм гарантирует допустимые значения индексов.

Рис. 3.1. Хеш-таблица
Чтобы вставить в таблицу новый элемент, мы хешируем ключ. Этим мы определяем список, в который его нужно добавить. Затем вставляем элемент в начало этого списка. Например, чтобы добавить 11, мы делим 11 на 8 и получаем остаток 3. Таким образом, 11 следует разместить в списке, на начало которого указывает hashTable[3]. Чтобы найти число, мы его хешируем и проходим по соответствующему списку. Чтобы удалить число, мы находим его и удаляем элемент списка, его содержащий.
При добавлении элементов в хеш-таблицу выделяются куски динамической памяти, которые организуются в виде связанных списков, каждый из которых соответствует входу хеш-таблицы. Этот метод называется связыванием. Другой метод, в котором все элементы располагаются в самой хеш-таблице, известен как прямая или открытая адресация; его описание вы найдете в цитируемой литературе.
Если хеш-функция распределяет совокупность возможных ключей равномерно по множеству индексов, то хеширование эффективно разбивает множество ключей. Наихудший случай - когда все ключи хешируются в один индекс. При этом мы работаем с одним линейным списком, который и вынуждены последовательно сканировать каждый раз, когда что-нибудь делаем. Отсюда видно, как важна хорошая хеш-функция. Здесь мы рассмотрим лишь несколько из возможных подходов. При иллюстрации методов предполагается, что unsigned char располагается в 8 битах, unsigned short int - в 16, unsigned long int - в 32.
Деление (размер таблицы hashTableSize - простое число). Этот метод использован в последнем примере. Хеширующее значение hashValue, изменяющееся от 0 до (hashTableSize - 1), равно остатку от деления ключа на размер хеш-таблицы. Вот как это может выглядеть:
typedef int HashIndexType;
HashIndexType Hash(int Key) {
return Key % HashTableSize;
}
Для успеха этого метода очень важен выбор подходящего значения hashTableSize. Если, например, hashTableSize равняется двум, то для четных ключей хеш-значения будут четными, для нечетных - нечетными. Ясно, что это нежелательно - ведь если все ключи окажутся четными, они попадут в один элемент таблицы. Аналогично, если все ключи окажутся четными, то hashTableSize, равное степени двух, попросту возьмет часть битов Key в качестве индекса. Чтобы получить более случайное распределение ключей, в качестве hashTableSize нужно брать простое число, не слишком близкое к степени двух.
Мультипликативный метод (размер таблицы hashTableSize есть степень 2n). Значение key умножается на константу, затем от результата берется необходимое число битов. В качестве такой константы Кнут рекомендует золотое сечение (sqrt(5) - 1)/2 = 0.6180339887499. Пусть, например, мы работаем с байтами. Умножив золотое сечение на 28, получаем 158. Перемножим 8-битовый ключ и 158, получаем 16-битовое целое. Для таблицы длиной 25 в качестве хеширующего значения берем 5 младших битов младшего слова, содержащего такое произведение. Вот как можно реализовать этот метод:
/* 8-bit index */
typedef unsigned char HashIndexType;
static const HashIndexType K = 158;
/* 16-bit index */
typedef unsigned short int HashIndexType;
static const HashIndexType K = 40503;
/* 32-bit index */
typedef unsigned long int HashIndexType;
static const HashIndexType K = 2654435769;
/* w=bitwidth(HashIndexType), size of table=2**m */
static const int S = w - m;
HashIndexType HashValue = (HashIndexType)(K * Key) S;
Пусть, например, размер таблицы hashTableSize равен 1024 (210). Тогда нам достаточен 16-битный индекс и S будет присвоено значение 16 - 10 = 6. В итоге получаем:
typedef unsigned short int HashIndexType;
HashIndexType Hash(int Key) {
static const HashIndexType K = 40503;
static const int S = 6;
return (HashIndexType)(K * Key) S;
}
Аддитивный метод для строк переменной длины (размер таблицы равен 256). Для строк переменной длины вполне разумные результаты дает сложение по модулю 256. В этом случае результат hashValue заключен между 0 и 255.
typedef unsigned char HashIndexType;
HashIndexType Hash(char *str) {
HashIndexType h = 0;
while (*str) h += *str++;
return h;
}
Исключающее ИЛИ для строк переменной длины (размер таблицы равен 256). Этот метод аналогичен аддитивному, но успешно различает схожие слова и анаграммы (аддитивный метод даст одно значение для XY и YX). Метод, как легко догадаться, заключается в том, что к элементам строки последовательно применяется операция "исключающее или". В нижеследующем алгоритме добавляется случайная компонента, чтобы еще улучшить результат.
typedef unsigned char HashIndexType;
unsigned char Rand8[256];
HashIndexType Hash(char *str) {
unsigned char h = 0;
while (*str) h = Rand8[h ^ *str++];
return h;
}
Здесь Rand8 - таблица из 256 восьмибитовых случайных чисел. Их точный порядок не критичен. Корни этого метода лежат в криптографии; он оказался вполне эффективным (Pearson [1990]).
Исключающее ИЛИ для строк переменной длины (размер таблицы <= 65536). Если мы хешируем строку дважды, мы получим хеш-значение для таблицы любой длины до 65536. Когда строка хешируется во второй раз, к первому символу прибавляется 1. Получаемые два 8-битовых числа объединяются в одно 16-битовое.
typedef unsigned short int HashIndexType;
unsigned char Rand8[256];
HashIndexType Hash(char *str) {
HashIndexType h;
unsigned char h1, h2;
if (*str == 0) return 0;
h1 = *str; h2 = *str + 1;
str++;
while (*str) {
h1 = Rand8[h1 ^ *str];
h2 = Rand8[h2 ^ *str];
str++;
}
/* h is in range 0..65535 */
h = ((HashIndexType)h1 << 8)|(HashIndexType)h2;
/* use division method to scale */
return h % HashTableSize
}
Размер хеш-таблицы должен быть достаточно большим, чтобы в ней оставалось разумно большое число пустых мест. Как видно из таблицы 3.1, чем меньше таблица, тем больше среднее время поиска ключа в ней. Хеш-таблицу можно рассматривать как совокупность связанных списков. По мере того, как таблица растет, увеличивается количество списков и, соответственно, среднее число узлов в каждом списке уменьшается. Пусть количество элементов равно n. Если размер таблицы равен 1, то таблица вырождается в один список длины n. Если размер таблицы равен 2 и хеширование идеально, то нам придется иметь дело с двумя списками по n/100 элементов в каждом. Это сильно уменьшает длину списка, в котором нужно искать. Как мы видим в таблице 3.1, имеется значительная свободы в выборе длины таблицы.
Таблица 3.1. Зависимость среднего времени поиска (µs) от hashTableSize. Сортируются 4096 элементов.
Размер |
Время |
|
Размер |
Время |
1 |
869 |
|
128 |
9 |
2 |
432 |
|
256 |
6 |
4 |
214 |
|
512 |
4 |
8 |
106 |
|
1024 |
4 |
16 |
54 |
|
2048 |
3 |
32 |
28 |
|
4096 |
3 |
64 |
15 |
|
8192 |
3 |