
Дональд Кнут. Искусство программирования. т.3 / knuth3
.pdf
Original pages: 483-535 261
Теорема B. При 2k−1 N < 2k удачный поиск, использующий алгоритм B, требует (min1; maxk)срав-
нений. Неудачный поиск требует k сравнений при N = 2k − 1 либо k − 1 или k сравнений при
2k−1 N < 2k − 1.
Дальнейший анализ бинарного поиска. [Рекомендуем не интересующимся математикой перейти сразу к соотношениям (4).] Представление в виде дерева позволяет легко подсчитать среднее число сравнений. Через CN обозначим среднее число сравнений при удачном поиске в предположении, что каждый из N ключей с равной вероятностью является аргументом поиска. Среднее число сравнений CN0 соответствует неудачному поиску; предполагается, что все интервалы (их N + 1) между ключами и вне крайних значений равновероятны. Имеем по определению длин внутреннего и внешнего пути
CN |
= 1 + |
Длина внутреннего пути дерева |
; |
||||
N |
|||||||
|
|
|
|
|
|||
C |
0 |
= |
Длина внешнего пути дерева |
: |
|
||
|
N |
|
|
N + 1 |
|
||
|
|
|
|
|
Из формулы (2.3.4.5-3) видно, что длина внешнего пути на 2N больше длины внутреннего пути; отсюда следует довольно неожиданное соотношение между CN и CN0
CN = 1 + |
1 |
CN0 − 1: |
(2) |
N |
Эта формула, полученная Хиббардом [JACM, 9 (1962), 16–17], справедлива для всех методов поиска, соответствующих бинарным деревьям, т. е. для всех методов, не содержащих лишних сравнений. Дисперсия CN также может быть выражена через дисперсию CN0 (см. упр. 25).
Из приведенных формул видно, что ”наилучшему” способу поиска путем сравнений соответствует дерево с минимальной длиной внешнего пути среди всех бинарных деревьев, содержащих N внутренних узлов. К счастью, можно доказать, что алгоритм B оптимален в этом смысле, так как бинарное дерево имеет минимальную длину пути тогда и только тогда, когда все внешние узлы находятся на одном или двух соседних уровнях. (См. упр. 5.3.1-20.) Следовательно, длина внешнего пути бинарного дерева, соответствующего алгоритму B, равна
(N + 1)(blog2 Nc + 2) − 2blog2 Nc+1: |
(3) |
(См. (5.3.1-33).) Используя (3) и (2), можно точно вычислить среднее число сравнений, если предположить, что все аргументы поиска равновероятны:
N |
= |
1 2 3 |
4 |
5 6 7 |
8 9 |
|
10 |
11 |
12 |
13 |
14 |
15 |
16 |
||||||
CN = 1 121 132 2 |
251 262 273 285 297 |
2 |
9 |
3 |
|
3 |
1 |
3 |
2 |
3 |
3 |
3154 |
3 |
6 |
|||||
10 |
12 |
13 |
14 |
16 |
|||||||||||||||
CN0 |
= 1 132 2 |
252 264 276 3 |
392 3 |
4 |
3 |
6 |
3 |
8 |
31310 |
31412 |
31514 |
4 |
4 |
2 |
|||||
10 |
11 |
12 |
17 |
В общем случае, если k = blog2 Nc, имеем (ср. с (5.3.1-34))
|
|
k+1 |
− k − 2)=N |
= log2 N − 1 + " + (k + 2)=N; |
|
|
CN |
= k + 1 |
− (2k |
|
(4) |
||
CN0 |
= k + 2 |
− 2 |
+1=(N + 1) |
= log2 N + "0; |
|
где 0 ", "0 < 0:0861.
Итак, алгоритм B требует максимум blog2 Nc+1 сравнений; среднее число сравнений при удачном поиске приближенно равно log2 N − 1. Ни один метод, основанный на сравнении ключей, не может дать лучших результатов. Среднее время работы программы В составляет примерно
(18 log2 N − 15)u для удачного поиска;
(5)
(18 log2 N + 13)u для неудачного поиска
(предполагается, что все исходы поиска равновероятны).
Одна важная модификация. Соблазнительно вместо трех указателей l, i, uиспользовать лишь два: текущее положение i и величину его изменения ; после каждого сравнения, не давшего равенства, мы могли бы установить i i и =2 (приблизительно). Этот путь реализуем, но он требует особой аккуратности в деталях, как в приведенном ниже алгоритме; более простые подходы обречены на неудачу!
Алгоритм U. (Однородный бинарный поиск.) Алгоритм служит для отыскания аргумента K в таблице записей R1, R2, : : : , RN, ключи которых расположены в возрастающем порядке: K1 < K2 <
262Original pages: 483-535
: : : < KN. При четном N иногда происходит обращение к фиктивному ключу K0, который необходимо
установить равным −1 (или любой величине, меньшей K. Предполагается, что N 1.
U1 |
[Начальная установка.] Установить i dN=2e, m bN=2c. |
||
U2 |
[Сравнение.] Если K < Ki, то перейти на U3; если K > Ki, то перейти на U4; при K = Ki алгоритм |
||
|
оканчивается удачно. |
||
U3 |
[Уменьшение i.] (Мы определили положение интервала, где нужно продолжать поиск. Он содер- |
||
|
жит m или m − 1 записей; i указывает на первый элемент справа от интервала.) Если m = 0, то |
||
|
алгоритм оканчивается неудачно. В противном случае установить i i − dm=2e; m bm=2c и |
||
|
вернуться на U2. |
||
U4 |
[Увеличение i.] (Ситуация та же, что и в шаге U3, только i указывает на первый элемент слева |
||
|
от интервала.) Если m = 0, то алгоритм оканчивается неудачно. В противном случае установить |
||
|
i i + dm=2e; m bm=2c и вернуться на U2. |
|
|
На рис. 6 представлено бинарное дерево, соответствующее поиску при N = 10. При неудачном поиске как раз перед окончанием работы алгоритма может производиться лишнее сравнение; узлы, отвечающие этим сравнениям, заштрихованы. Данный процесс поиска можно назвать однородным, так как разность между числом в узле уровня ` и числом в узле-предшественнике уровня ` − 1 есть постоянная величина для всех узлов уровня `. Обосновать правильность алгоритма U можно следующим образом. Предположим, что поиск нужно произвести в интервале
Picture: Рис. 6. Бинарное дерево для ”однородного” бинарного поиска (N = 10).
длины n − 1; сравнение со средним элементом (если n четно) или с одним из двух средних (если n
нечетно) |
выделяет два интервала длины |
b |
n= |
2c−1 |
и n= |
2e−k 1 |
. После повторения этой процедуры k раз |
|||||
k |
|
d |
|
|
− 1 и максимальной длиной n=2 |
k |
− 1. |
|||||
мы получим 2 интервалов с минимальной длиной |
|
n=2 |
|
|||||||||
Следовательно, на каждом этапе длины двух |
интервалов различаются самое большее на 1, что делает |
|||||||||||
|
|
|
|
|
|
|
|
возможным выбор подходящего ”среднего” элемента без запоминания последовательности точных значений длин.
Важное преимущество алгоритма U состоит в том, что нам совсем не нужно сохранять значение m; нужно лишь ссылаться на коротенькую таблицу значений для каждого уровня. Таким образом, алгоритм сводится к следующей процедуре, одинаково хорошей и для двоичных, и для десятичных ЭВМ.
Алгоритм C. (Однородный бинарный поиск.) Алгоритм аналогичен алгоритму U, но вместо вычислений, относящихся к m, использует вспомогательную таблицу величин
|
DELTA[j] = |
N |
+ 2 |
j |
− |
1 |
= |
N |
округленное; |
|
|
||
|
|
|
1 j blog2 Nc + 2: |
(6) |
|||||||||
|
|
|
|
|
|||||||||
|
|
2j |
|
|
|
2j |
|||||||
C1 |
[Начальная установка.] Установить i |
|
DELTA[1], j 2. |
|
|
||||||||
C2 |
[Сравнение:] Если K < Ki, то перейти на C3; если K > Ki, то перейти на C4. При K = Ki алгоритм |
||||||||||||
|
оканчивается удачно. |
|
|
|
|
|
|
|
|
|
|
|
|
C3 |
[Уменьшение i.] Если DELTA[j] = 0, то алгоритм оканчивается неудачно. В противном случае |
||||||||||||
|
установить i i − DELTA[j], j |
|
|
j + 1 и вернуться на C2. |
|
|
|||||||
C4 |
[Увеличение i.] Если DELTA[j] = 0, алгоритм оканчивается неудачно. В противном случае устано- |
||||||||||||
|
вить i i + DELTA[j], j |
j + 1 и вернуться на C2. |
|
|
|
|
|||||||
|
|
|
|
В упр. 8 будет показано, что алгоритм ссылается на вспомогательный ключ K0 = −1 лишь при четных N.
Программа C. (Однородный бинарный поиск.) Эта программа на базе алгоритма C проделывает ту же работу, что и программа B. Используются rA K, rI1 i, rI2 j, rI3 DELTA[j].
START |
ENT1 |
N+1/2 |
1 |
C1. |
Начальная установка. |
|
ENT2 |
2 |
1 |
j |
2. |
|
LDA |
К |
1 |
|
|
|
JMP |
2F |
1 |
|
|
3H |
JE |
SUCCESS |
C1 |
Переход, если K = Ki. |
|
|
J3Z |
FAILURE |
C1 − S |
Переход, если DELTA[j] = 0. |
|
|
DEC1 |
0,3 |
C1 − S − A C3. |
Уменьшение i. |
|
5H |
INC2 |
1 |
C − 1 |
j |
j + 1. |

Original pages: 483-535 263
2H |
LD3 |
DELTA,2 |
C |
C2. Сравнение. |
|
|
|
СМРА |
KEY.1 |
C |
Переход, если K Ki. |
|
|
JLE |
3B |
C |
|
|
|
INC1 |
0,3 |
C2 |
C4. Увеличение i. |
|
|
J3NZ |
5B |
C2 |
Переход, если DELTA[j] 6= 0. |
FAILURE EQU |
* |
1 − S |
Выход, если нет в таблице. |
||
|
|
|
|
|
|
При удачном поиске этот алгоритм соответствует бинарному дереву с той же длиной внутреннего пути, что и алгоритм В, поэтому среднее число сравнений CN дается формулой (4). При неудачном поиске алгоритм C всегда совершает ровно blog2 Nc+1 сравнений. Полное время работы программы C не вполне симметрично по отношению к левым и правым ветвям, так как C1 имеет вес, больший чем C2, но в упр. 9 будет показано, что случай K > Ki встречается примерно так же часто, как и K < Ki; следовательно, программа C требует приблизительно
(8:5 log2 N − 6)u |
на удачный поиск; |
(7) |
(8:5blog2 Nc + 12)u на неудачный поиск. |
|
Это более чем в два раза быстрее программы B, причем не используются специальные свойства двоичных ЭВМ, в то время как в формуле (5) предполагается, что MIX имеет команду ”двоичный сдвиг вправо”.
Другая модификация бинарного поиска, предложенная в 1971 г. Л. Э. Шером, на некоторых ЭВМ допускает еще более быструю реализацию, так как она однородна после первого
Picture: Рис. 7. Бинарное дерево для почта однородного поиска Шера (N = 10). |
|
|
|||||||||
шага и не требует вспомогательной таблицы. Сначала мы сравниваем |
K |
и |
K |
, где i |
|
k, k |
= blog2 |
N . |
|||
|
i |
|
k |
|
= 2k |
|
c |
||||
Если K < Ki, мы используем однородный поиск с последовательностью = 2 |
|
−1, 2 |
|
−2, : : :, 1, 0. С |
|||||||
другой стороны, при K > Ki и N > 2k устанавливаем i = i0 = N + 1 − 2`, где ` = |
|
log2(N `− 2k)` + 1, и, |
делая вид, что первым сравнением было K > Ki0, используем однородный поиск с = 2 −1, 2 −2, : : : ,
1, 0.
Рисунок 7 иллюстрирует метод Шера при N = 10. В методе Шера никогда не требуется более blog2 Nc+1 сравнений; следовательно, он дает одно из лучших средних чисел сравнения, несмотря на то что иногда проходит через несколько последовательных избыточных шагов (ср. с упр. 12).
Еще одна модификация бинарного поиска, улучшающая все рассмотренные методы при очень больших N, обсуждается в упр. 23. В упр. 24 изложен еще более быстрый метод!
Фибоначчиев поиск. При рассмотрении многофазного слияния (п. 5.4.2) мы видели, что числа Фибоначчи могут играть роль, аналогичную степеням 2. Похожее явление имеет место и в случае поиска, когда с помощью чисел Фибоначчи создаются методы, способные соперничать с бинарным поиском. Предлагаемый метод для некоторых ЭВМ предпочтительнее бинарного, так как он включает лишь сложение и вычитание; нет необходимости в делении на 2. Следует отличать процедуру, которую мы собираемся обсуждать, от известной численной процедуры ”фибоначчиева поиска”, которая используется для нахождения максимума одновершинной функции [ср. с Fibonacci Quarterly, 4 (1966), 265–269]; схожесть названий ведет к некоторой путанице.
На первый взгляд фибоначчиев поиск кажется весьма таинственным; если просто взять программу и попытаться объяснить ее работу, то создастся впечатление, что она работает с помощью магии. Но туман таинственности рассеется, как только мы построим
Picture: |
Рис. 8. Дерево Фибоначчи порядка 6. |
соответствующее дерево поиска. Поэтому изучение рассматриваемого метода начнем с рассказа о ”фибоначчиевых деревьях”.
На рис. 8 изображено дерево Фибоначчи порядка 6. Заметьте, что оно несколько больше напоминает реальный куст, чем рассматривавшиеся ранее деревья, возможно, потому, что многие природные процессы удовлетворяют закону Фибоначчи. Вообще фибоначчиево дерево порядка k имеет Fk+1 − 1 внутренних (круглых) узлов и Fk+1 внешних (квадратных) узлов; оно строится следующим образом:
Если k = 0 или k = 1, дерево сводится просто к 0 .
Если k 2, корнем является (Fk); левое поддерево есть дерево Фибоначчи порядка k−1; правое поддерево есть дерево Фибоначчи порядка k − 2 с числами в узлах, увеличенными на Fk.
264Original pages: 483-535
Заметим, что, за исключением внешних узлов, числа, соответствующие преемникам каждого внутреннего узла, отличаются от числа в этом узле на одну и ту же величину, а именно на число Фибо-
наччи. Так, 5 = 8 − F4, 11 = 8 + F4 (рис. 8). Если разность была равна Fj, то на следующем уровне соответствующая разность составит для левой ветви Fj−1, а для правой Fj−2. Так, например, 3 = 5−F3, a 10 = 11 − F2.
Эти наблюдения в совокупности с подходящим механизмом распознавания внешних узлов дают
|
Алгоритм F. (Фибоначчиев поиск.) Алгоритм предназначается для поиска аргумента K в таблице |
|
записей R1, R2, : : : , RN, расположенных в порядке возрастания ключей K1 < K2 < : : : < KN. |
|
|
|
Для удобства описания предполагается, что N + 1 есть число Фибоначчи Fk+1. Подходящей на- |
|
чальной установкой данный метод можно сделать пригодным для любого N (см. упр. 14). |
|
|
F1 |
[Начальная установка.] Установитьi Fk, p Fk−1, q Fk−2. (В этом алгоритме pи q обозначают |
|
|
последовательные числа Фибоначчи.) |
|
F2 |
[Сравнение.] Если K < Ki, то перейти на F3; если K > Ki, то перейти на F4; если K = Ki, |
|
|
алгоритм заканчивается удачно. |
i−q, |
F3 |
[Уменьшение i.] Если q = 0, алгоритм заканчивается неудачно. Если q 6= 0, то установить i |
|
|
заменить (p; q) на (q; p − q) и вернуться на F2. |
|
F4 |
[Увеличение i.] Если p = 1, алгоритм заканчивается неудачно. Если p 6= 1, установить i |
i + q, |
pp − q, q q − p и вернуться на F2.
Вприводимой ниже реализации алгоритма F для машины MIX скорость увеличивается за счет дублирования внутреннего цикла, в одном из экземпляров которого p хранится в rI2, а q—в rI3,
вдругом же регистры меняются ролями; это упрощает шаг F3. На самом деле программа хранит в регистрах p − 1 и q − 1, что упрощает проверку ”p = 1?” в шаге F4.
Программа F. (Фибоначчиев поиск.) Значения регистров: rA K, rI1 i, (rI2; rI3) p − l,
(rI3; rI2) q − l.
START LDA |
K |
1 |
F1. Начальная установка. |
|
ENT1 |
Fk |
1 |
i |
Fk. |
ENT2 |
Fk−1 − 1 1 |
p |
Fk−1. |
|
ENT3 |
Fk−2 − 1 1 |
q |
Fk−2. |
|
|
JMP |
F2A |
1 На F2. |
|
|
|
|
|
|
|
F4A |
INC1 |
1,3 |
F4B |
INC1 |
1,2 |
C2 |
− S − A F4. |
Увеличение i. i |
i + q. |
||
|
|
DEC2 |
1,3 |
|
DEC3 |
1,2 |
G2 |
− S − A p |
p − q. |
|
|
|
|
DEC3 |
1,2 |
|
DEC2 |
1,3 |
G2 |
− S − A q |
q − p. |
|
|
F2A |
CMPA |
KEY, 1 |
F2В |
CMPA |
KEY,1 |
|
G |
F2. |
Сравнение. |
|
|
|
|
JL |
F3A |
|
JL |
F3B |
|
G |
На FЗ, если K < Ki. |
|
|
|
|
JE |
SUCCESS |
|
JE |
SUCCESS |
|
G2 |
Выход, если K = Ki. |
|
|
|
|
J2NZ |
F4A |
|
J3NZ |
F4B |
C2 − S |
На F4, если p 6= 1. |
|
||
|
|
JMP |
FAILURE |
|
JMP |
FAILURE |
|
A |
Выход, если нет в таблице. |
||
F3A |
DEC1 |
1,3 |
F3B |
DEC1 |
1,2 |
|
C1 |
F3. |
Уменьшение i. i |
i − q. |
|
|
|
DEC2 |
1,3 |
|
DEC3 |
1,2 |
|
C1 |
p |
p − q. |
|
|
|
J3NN |
F2B |
|
J2NN |
F2A |
|
C1 |
Смена регистров, если q > 0. |
||
|
|
JMP |
FAILURE |
|
JMP |
FAILURE |
1 − S − A |
Выход, если нет в таблице. |
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
В упр. 18 анализируется время работы этой программы. Рисунок 8 показывает, а анализ доказывает, что влево мы идем несколько чаще, чем вправо. Через C, C1 и C2 − S обозначим число выполнений шагов F2, F3 и F4 соответственно. Имеем
C = (ave k=sqrt5 + O(1); maxk − 1);
|
|
p |
|
|
|
|
|
|
|
|
|
C1 = (avek= |
5 + O(1); maxk − 1); |
(8) |
|||||||
C2 |
|
1 |
|
|
p |
|
|
|
k=2 ): |
|
|
|
|
|
|
||||||
− |
S = (ave − |
k= 5 + O(1); max |
b |
|||||||
|
|
|
|
|
|
|
|
c |
Значит, левая ветвь выбирается примерно в = 1:618 раза чаще правой (это можно было предвидеть, так как каждая проба делит рассматриваемый интервал на две части, причем левая часть примерно

Original pages: 483-535 265
в раз длиннее правой). Среднее время работы программы F составляет примерно
|
p |
|
|
|
для удачного поиска; |
|
|
(6 k= |
|
|
5 − (2 + 22 )=5)u (6:252 log2 |
N − 4:6)u |
(9) |
||
p |
|
|
|
для неудачного поиска. |
|
||
(6 k= |
|
5 + (58=(27 ))=5)u (6:252 log2 |
N + 5:8)u |
|
Это несколько лучше, чем (7), хотя в наихудшем случае программа F работает примерно (8:6 log2 N)u, т.е. чуть-чуть медленнее программы С.
Интерполяционный поиск. Забудем на минуту о вычислительных машинах и проанализируем, как производит поиск человек. Иногда повседневная жизнь подсказывает путь к созданию хороших алгоритмов.
Представьте, что вы ищете слово в словаре. Маловероятно, что вы сначала заглянете в середину словаря, затем отступите от начала на 1=4 или 3=4 и т. д., как в бинарном поиске, и уж совсем невероятно, что вы воспользуетесь фибоначчиевым поиском!
Если нужное слово начинается с буквы A, вы, по-видимому, начнете поиск где-то в начале словаря. Во многих словарях имеются ”побуквенные высечки” для большого пальца, которые показывают страницу, где начинаются слова на данную букву. Такую пальцевую технику легко приспособить к ЭВМ, что ускорит поиск; соответствующие алгоритмы исследуются в x 6.3.
Когда найдена отправная точка для поиска, ваши дальнейшие действия мало похожи на рассмотренные методы. Если вы заметите, что нужное слово должно находиться гораздо дальше открытой страницы, вы пропустите порядочное их количество, прежде чем сделать следующую попытку. Это в корне отличается от предыдущих алгоритмов, которые не делают различия между ”много больше” и ”чуть больше”.
Мы приходим к такому алгоритму, называемому ”интерполяционным поиском”: если известно, что K лежит между Kl и Ku, то следующую пробу делаем примерно на расстоянии (u − l)(K − Kl)=(Ku − Kl) от l, предполагая, что ключи являются числами, возрастающими приблизительно как арифметическая прогрессия.
Интерполяционный поиск асимптотически предпочтительнее бинарного; по существу, один шаг
бинарного поиска уменьшает количество ”подозреваемых” записей с n до 1n, а один шаг интерполя-
2 p
ционного (если ключи в таблице распределены случайным образом)—с n до n. Можно показать, что интерполяционный поиск требует в среднем около log2 log2 N шагов (упр. 22).
К сожалению, эксперименты на ЭВМ показали, что интерполяционный поиск уменьшает число сравнений не настолько, чтобы компенсировать возникающий дополнительный расход времени, когда таблица, в которой производится поиск, хранится во внутренней (”быстрой”) памяти. Разность между log2 log2 N и log2 N становится существенной лишь для весьма больших N, а типичные файлы недостаточно случайны. Интерполяция успешна до некоторой степени лишь в применении к поиску на внешних запоминающих устройствах. (Заметим, что ручной просмотр словаря есть, в сущности, не внутренний, а внешний поиск; это является темой последующих рассмотрений.)
История и библиография. Первым известным примером длинного перечня элементов, упорядоченных для облегчения поиска, является знаменитая вавилонская таблица обратных величин Inakibit-Anu, датируемая примерно 200 г. до н.э. Эта глиняная пластинка, очевидно, открывала серию из трех табличек, содержащих более 500 многозначных шестидесятеричных чисел и обратных им величин, отсортированных в лексикографическом порядке. Например, встречается такая последовательность:
01 |
13 |
09 |
34 |
29 |
08 |
08 |
53 |
20 |
49 |
12 |
27 |
|
|
|
|
|
|
01 |
13 |
14 |
31 |
52 |
30 |
|
|
|
49 |
09 |
07 |
12 |
|
|
|
|
|
01 |
13 |
43 |
40 |
48 |
|
|
|
|
48 |
49 |
41 |
15 |
|
|
|
|
|
01 |
13 |
48 |
40 |
30 |
|
|
|
|
48 |
46 |
22 |
59 |
25 |
25 |
55 |
33 |
20 |
01 |
14 |
04 |
26 |
40 |
|
|
|
|
48 |
36 |
|
|
|
|
|
|
|
Сортировка 500 подобных чисел средствами тех времен кажется феноменальной. [См. также D. Е. Knuth, CACM, 15 (1972), 671–677, где содержится много дополнительных сведений.]
Довольно естественно располагать по порядку числа, но отношение порядка между буквами или словами не представляется само собой очевидным. Однако последовательности для сравнивания букв присутствовали уже в наиболее древних алфавитах. Например, многие библейские псалмы содержат стихи, следующие друг за другом в строго алфавитном порядке: первый стих начинается с алефа, второй с бета и т. д.; это облегчало запоминание. Иногда стандартная последовательность букв использовалась семитами и греками для обозначения чисел; например, , , γ обозначали 1, 2, 3 соответственно.

266 Original pages: 483-535
Однако использование алфавитного порядка для слов целиком было, вероятно, гораздо более поздним изобретением; вещи, очевидные сейчас даже для ребенка, когда-то требовалось объяснять взрослым! Некоторые документы, датируемые примерно 300 г. до н.э., найденные на островах Эгейского моря, содержат списки членов нескольких религиозных общин; они упорядочены, но только по первой букве, т. е. представляют собой результат лишь первого прохода слева направо поразрядной сортировки. Греческие папирусы 134–135 г. н. э. содержат фрагменты счетов, в которых фамилии налогоплательщиков упорядочены по первым двум буквам. Аполлоний Софист2использовал алфавитное упорядочение по первым двум, а часто и по последующим буквам в своем ”Словаре гомеровских слов” (I век н. э.). Известно несколько примеров более совершенного алфавитного упорядочения, скажем выдающиеся ”Комментарии к Гиппократу” Галена3(около 200 г.), но это было очень редким явлением. Так, в хронике Св. Исидора4”Etymologiarum” (около 630 г., книга X) слова упорядочены лишь по первой букве, а в наиболее раннем из известных двуязычных словарей ”Corpus Glossary” (около 725 г.) — только по двум первым. Две последние работы были, вероятно, крупнейшими нечисловыми файлами данных, скомпилированными в средние века.
Только в книге Джованни Генуэзского ”Catholicon” (1286 г.) мы находим подробное описание правильного алфавитного порядка. В предисловии Джованни объясняет, что
amo |
предшествует bibo |
abeo |
предшествует adeo |
amatus |
предшествует amor |
imprudens |
предшествует impudens |
iusticia |
предшествует iustus |
polisintheton |
предшествует polissenus |
(т. е. приводит примеры ситуаций, когда порядок определяется по 1, 2, : : : , 6-й буквам) ”и далее аналогично”. Он замечает, что открытие этого правила потребовало значительных усилий. ”Я прошу Вас, уважаемый читатель, не презирать эту мою большую работу и этот порядок как нечто ничего не стоящее”. Развитие алфавитного порядка до момента изобретения книгопечатания подробно изучил Л. У. Дейли (Collection Latomus, 90 (1967), 100 pp.). Он обнаружил несколько интересных старинных рукописей, которые, несомненно, использовались как черновики при сортировке слов по первой букве (см. стр. 87–90 его монографии).
Первый словарь английского языка Роберта Кодри ”Table Alphabeticall” (London, 1604) содержит следующие инструкции:
Если слово, которое тебе нужно найти, начинается с (a), смотри в начало этой книги, а если с (v)—то в конец. Опять если слово начинается с (ca), смотри в начало буквы (c), а если с (cu) то в конец. И так до конца слова.
Интересно заметить, что Кодри во время подготовки словаря, вероятно, сам учился расставлять слова в алфавитном порядке; на нескольких первых страницах встречается много неправильно стоящих слов, зато дальше число ошибок существенно уменьшается.
Бинарный поиск впервые упомянут Джоном Мочли в дискуссии, которая была, пожалуй, первым опубликованным обсуждением методов нечисленного программирования [см. Theory and techniques for the design of electronic digital computers, ed. by G. W. Patterson, 3 (1946); 22.8–22.9]. В течение 50- х годов метод становится ”хорошо известным”, но, кажется, никто не разрабатывал детали алгоритма для N 6= 2n − 1. [См. A. D. Booth, Nature, 176 (1955), 565; A. I. Dumey, Computers and Automation, 5 (December, 1956), 7, где бинарный поиск имеет название ”Двадцать вопросов”; D. D. McCracken, Digital Computer Programming (Wiley, 1957), 201–203; M. Halpern, CACM 1, 2 (February, 1958), 1–3.]
По-видимому, алгоритм бинарного поиска, пригодный для всех N, впервые опубликован Боттенбруком [JACM, 9 (1962), 214]. Он представил интересную модификацию алгоритма B, когда проверки на равенство отодвигаются в конец алгоритма. Используя в шаге B2 i d(l + u)=2e вместо b(l + u)=2c, он устанавливает l i при K Ki; тогда u−l уменьшается после каждого шага. В конце, когда l = u, имеем Kl K < Kl+1, и можно проверить, был ли поиск удачным, произведя еще одно сравнение. (Он предполагал, что первоначально K K1.) Эта идея позволяет несколько ускорить внутренний цикл на многих ЭВМ; то же верно и для всех обсуждавшихся в данном пункте алгоритмов, однако такое изменение оправдано лишь для больших N (см. упр. 23).
К. Э. Айверсон [A Programming Language (Wiley, 1962), 141] привел процедуру алгоритма B, но без рассмотрения возможности неудачного поиска. Д. Кнут (CACM, 6 (1963), 556–558) представил
2 Один из грамматиков древности, родом из Александрии.—Прим. перев.
3 Римский врач и естествоиспытатель, классик античной медицины.—Прим. перев.
4 Исидор Севильский—испанский епископ, выдающийся ученый и писатель.—Прим. перев.

268 Original pages: 483-535
18. [М30] Проведите частотный анализ для программы F и найдите точные формулы для средних значений C1, C2 и A как функций от k, Fk, Fk+1 и S.
19. [М42] Проведите детальный анализ среднего времени работы алгоритма, предложенного в упр. 14.
20. [М22] Число сравнений, требующихся при бинарном поиске, приближенно равно log N, при p 2
фибоначчиевом—( = 5)log N. Цель этого упражнения—показать, что эти формулы являются частными случаями более общего результата.
Пусть p и q—положительные числа и p + q = 1. Рассмотрим алгоритм поиска по таблице из N записей с возрастающими ключами, который, начиная со сравнения аргумента с (pN)-м ключом, повторяет эту процедуру для меньших блоков. (Для бинарного поиска p = q = 1=2; для фибоначчиева p = 1= , q = 1= 2.)
Обозначим среднее число сравнений, требуемых для поиска в таблице размера N, через C(N); оно приближенно удовлетворяет соотношениям
C(1) = 0; C(N) = 1 + pC(pN) + qC(qN) для N > 1.
Это происходит потому, что после первого сравнения поиск примерно с вероятностью p сводится к поиску среди pN элементов и с вероятностью q—к поиску среди qN элементов. При больших N можно пренебречь эффектом низшего порядка, связанным с тем, что числа pN и qN не целые.
a)Покажите, что C(N) = logb N точно удовлетворяет указанным соотношениям при некотором b. Для бинарного и фибоначчиева поисков величина b получается из выведенных ранее формул.
b)Некто рассуждал так: ”С вероятностью p длина рассматриваемого интервала делится на 1=p, с вероятностью q—на 1=q. Следовательно интервал делится в среднем на p(1=p)+q(1=q) = 2, так что алгоритм в точности так же хорош, как и бинарный поиск, независимо от p и q”. Есть ли ошибка в этих рассуждениях?
21.[20] Нарисуйте бинарное дерево, соответствующее интерполяционному поиску при N = 10.
22.[М43] (Э. К. Яо и Ф. Ф. Яо.) Покажите,, что должным образом оформленный интерполяционный
поиск в среднем требует (асимптотически) log2 log2 N сравнений, если N отсортированных ключей имеют независимые равномерные распределения. Более того, все алгоритмы поиска по таким таблицам должны совершать в среднем log2 log2 N сравнений (оценка асимптотическая).
>23. [25] Алгоритм бинарного поиска Г. Боттенбрука, упомянутый в конце пункта, ”откладывает” проверки на равенство до самого конца поиска. (Во время работы алгоритма мы знаем, что Kl K < Ku+1, проверка на равенство проводится лишь при l = u.) Такой трюк сделал бы программу B чуть быстрее при больших N, так как мы избавились бы от команды ”JF,” во внутреннем цикле. (Однако эта идея практически нереальна, так как log2 N обычно мал; лишь при N > 236 компенсируется необходимость дополнительной итерации!)
Покажите, что любой алгоритм поиска, соответствующий бинарному дереву и разветвляющийся по трем направлениям (<, =, или >), можно переделать в алгоритм, разветвляющийся во внутренних узлах лишь по двум направлениям (< или ). В частности, модифицируйте таким способом алгоритм C.
>24. [23] Полное бинарное дерево является удобным способом представления в последовательных ячейках дерева с минимальной длиной пути. (Ср. с п. 2.3.4.5.) Придумайте эффективный метод поиска, основанный на таком представлении. [Указание: можно ли в бинарном поиске использовать умножение на 2 вместо деления на 2?]
>25. [М25] Предположим, что у бинарного дерева имеется ak внутренних и bk внешних узлов на уровне k, k = 0, 1, : : :. (Корень находится на нулевом уровне.) Так, для рис. 8 имеем (a0; a1; : : : ; a6) = (1; 2; 4; 4; 1; 0)и (b0; b1; : : :; b6) = (0; 0; 0; 4; 7; 2). (a) Покажите, что существует простое алгебраическое
соотношение, связывающее производящие функции A(z) = |
k akzk и B(z) = |
k bkzk. (b) Распре- |
|
деление вероятностей при удачном поиске по бинарному |
дереву имеет производящую функцию |
||
|
P |
P |
g(z) = zA(z)=N, а при неудачном поиске производящая функция есть h(z) = B(z)=(N + 1). (В обозначениях п. 6.2.1 CN = mean(g), CN0 = mean(h), а соотношение (2) связывает эти величины.) Найдите зависимость между var(g) и var(h).
26.[22] Покажите, что дерево Фибоначчи связано с сортировкой многофазным слиянием на трех лентах.
27.[M30] (X. С. Стоун и Дж. Лини.) Рассмотрим процесс поиска, основанный только на сравнениях ключей и использующий одновременно k процессоров. При каждом шаге поиска определяются k индексов i1, i2, : : :, ik и мы совершаем k одновременных сравнений: если K = Kj для некоторого j, поиск кончается удачно, в противном случае переходим к следующему шагу поиска, основываясь на 2k возможных исходах K < Kij или K > Kij , 1 j k.

Original pages: 483-535 269
Покажите, что такой процесс при N ! 1 должен совершать в среднем по крайней мере logk+1 N шагов. (Предполагается, что все ключи в таблице, также как и аргумент поиска, равновероятны.) (Значит, по сравнению с однопроцессорным бинарным поиском мы выигрываем в скорости не в k раз, как можно было бы ожидать, а лишь в log2(k + 1) раз. В этом смысле выгоднее каждый процессор использовать для отдельного поиска, а не объединять их.)
6.2.2.Поиск по бинарному дереву
Впредыдущем пункте мы видели, что использование неявной структуры бинарного дерева облегчает понимание бинарного и фибоначчиева поисков. Рассмотрение соответствующих деревьев позволило заключить, что при данном N среди всех методов поиска путем сравнения ключей бинарный поиск совершает минимальное число сравнений. Но методы предыдущего пункта предназначены главным образом для таблиц фиксированного размера, так как последовательное расположение записей делает вставки и удаления довольно трудоемкими. Если таблица динамически изменяется, то экономия от использования бинарного поиска не покроет затрат на поддержание упорядоченного расположения ключей.
Явное использование структуры бинарного дерева позволяет быстро вставлять и удалять записи
ипроизводить эффективный поиск по таблице. В результате мы получаем метод, полезный как для поиска, так и для сортировки. Такая гибкость достигается
Picture: |
Рис. 10. Бинарное дерево поиска. |
путем добавления в каждую запись двух полей для хранения ссылок.
Методы поиска по растущим таблицам, часто называют алгоритмами таблиц символов, так как ассемблеры, компиляторы и другие системные программы обычно используют их для хранения определяемых пользователем символов. Например, ключом записи в компиляторе может служить символический идентификатор, обозначающий переменную в некоторой программе на Фортране или Алголе, а остальные поля записи могут содержать информацию о типе переменной и ее расположении
впамяти. Или же ключом может быть символ программы для MIX, а оставшаяся часть записи может содержать эквивалент этого символа. Программы поиска с вставкой по дереву, которые будут описаны
вэтом пункте, отлично подходят для использования в качестве алгоритмов таблиц символов, особенно если желательно распечатывать символы в алфавитном порядке. Другие алгоритмы, таблиц символов описаны в x 6.3 и 6.4.
На рис. 10 изображено бинарное дерево поиска, содержащее названия одиннадцати знаков зодиака5. Если теперь, отправляясь от корня дерева, мы будем искать двенадцатое название, SAGITTARIUS, то увидим, что оно больше, чем CAPRICORN, поэтому нужно идти вправо; оно больше, чем PISCES,—снова идем вправо; оно меньше, чем TAURUS,—идем влево; оно меньше, чем SCORPIO,— и мы попадаем во внешний узел 8 . Поиск был неудачным; теперь по окончании поиска мы можем вставить SAGITTARIUS, ”подвязывая” его к дереву вместо внешнего узла 8 . Таким образом, таблица может расти без перемещения существующих записей. Рисунок 10 получен последовательной вставкой, начиная с пустого дерева, ключей CAPRICORN, AQUARIUS, PISCES, ARIES, TAURUS, GEMINI,
CANCER, LEO, VIRGO, LIBRA, SCORPIO в указанном порядке.
На рис. 10 все ключи левого поддерева корня предшествуют по алфавиту слову CAPRICORN, а в правом поддереве стоят после него. Аналогичное утверждение справедливо для левого и правого поддеревьев любого узла. Отсюда следует, что при обходе дерева в симметричном порядке ключи располагаются строго в алфавитном порядке:
AQUARIUS; ARIES; CANCER; CAPRICORN; GEMINI;
LEO; : : : ; VIRGO;
так как симметричный порядок основан на прохождении сначала левого поддерева каждого узла, затем самого узла, а затем его правого поддерева (ср. с п. 2.3.1).
Ниже дается подробное описание процесса поиска с вставкой.
Алгоритм Т. (Поиск с вставкой по дереву.) Дана таблица записей, образующих бинарное дерево. Производится поиск заданного аргумента K. Если его нет в таблице, то в подходящем месте в дерево вставляется новый узел, содержащий K.
5 Знаки зодиака, упорядоченные по месяцам: Козерог, Водолей, Рыбы, Овен, Телец, Близнецы, Рак, Лев, Дева, Весы, Скорпион, Стрелец,—Прим. перев.
270 Original pages: 483-535
Предполагается, что узлы дерева содержат по крайней мере следующие поля:
KEY(P) = ключ, хранящийся в узле NODE(P);
LLINK(P) = указатель на левое поддерево узла NODE(P);
RLINK(P) = указатель на правое поддерево узла NODE(P):
Пустые поддеревья (внешние узлы на рис. 10) представляются пустыми указателями . Переменная ROOT указывает на корень дерева. Для удобства предполагается, что дерево не пусто (ROOT 6= ), так как при ROOT = операции становятся тривиальными.
Т1 [Начальная установка.] Установить P ROOT. (Указатель P будет продвигаться вниз по дереву.) Т2 [Сравнение.] Если K < KEY(P), то перейти на Т3; если K > KEY(P), то перейти на Т4; если K =
KEY(P), поиск завершен удачно. |
|
Т3 [Шаг влево.] Если LLINK(P) 6= , установить P |
LLINK(P) и вернуться на Т2. В противном случае |
перейти на Т5. |
|
Т4 [Шаг вправо.] Если RLINK(P) 6= , установить P |
RLINK(P) и вернуться на Т2. |
Т5 [Вставка в дерево.] (Поиск неудачен; теперь мы поместим K в дерево.) Выполнить Q ( AVAIL; Q теперь указывает на новый узел. Установить KEY(Q) K, LLINK(Q) RLINK(Q) . (На самом деле нужно произвести начальную установку и других полей нового узла.) Если K было меньше KEY(P), установить LLINK(P) Q; в противном случае установить RLINK(P) Q. (В этот момент мы могли бы присвоить P Q и удачно завершить работу алгоритма.)
Алгоритм сам подсказывает реализацию на языке MIXAL. Предположим, например, что узлы дерева имеют следующую структуру:
Picture: Рис. стр. 505
Возможно, далее расположены дополнительные слова INFO. Как и в гл. 2, AVAIL есть список свободной памяти. Итак, получается следующая программа для MIX.
Программа Т. (Поиск с вставкой по дереву.) rA K, rI1 P, rI2 Q.
LLINK |
EQU |
2:3 |
|
|
|
|
|
|
RLINK |
EQU |
4:5 |
|
|
|
|
|
|
START |
LDA |
К |
1 |
Т1. Начальная установка. |
||||
|
|
LD1 |
ROOT |
1 |
P |
ROOT. |
|
|
|
|
JMP |
2F |
1 |
|
|
|
|
4H |
LD2 |
0,1(RLINK) |
C2 |
T4. Шаг вправо. Q |
RLlNK(P). |
|||
|
|
J2Z |
5F |
C2 |
На T5, если Q = . |
|
||
1H |
ENT1 |
0,2 |
C − l |
P |
Q. |
|
|
|
2H |
CMPA |
1,1 |
C |
T2. Сравнение. |
|
|||
|
|
JG |
4В |
C |
На T4, если K > KEY(P). |
|||
|
|
JE |
SUCCESS |
C1 |
Выход, если K = KEY(P). |
|||
|
|
LD2 |
0,1 (LLINK) |
C1 − S |
TЗ. Шаг влево. Q |
LLINK(P). |
||
|
|
J2NZ |
1B |
C1 − S |
На T2, если Q 6= . |
|
||
5Н |
LD2 |
AVAIL |
1 − S |
T5 Вставки в дерево. |
||||
|
|
J2Z |
OVERFLOW |
1 − S |
|
|
|
|
|
|
LDX |
0,2(RLINK) |
1 − S |
Q ( AVAIL. |
|
|
|
|
|
STX |
AVAIL |
1 − S |
|
|
||
|
|
STA |
1,2 |
1 − S |
KEY(Q) K. |
|
. |
|
|
|
STZ |
0,2 |
1 − S |
LLINK(Q) |
RLINK(Q) |
||
|
|
JL |
1F |
1 − S |
K был меньше KEY(P)? |
|||
|
|
ST2 |
0,1(RLINK) |
A |
RLINK(P) |
Q. |
|
|
|
|
JMP |
*+2 |
A |
|
|
|
|
1H |
ST2 |
0,1(LLINK) |
1 − S − A LLINK(P) |
Q. |
|
|||
DONE |
EQU |
* |
1 − S Выход после вставки. |
|||||
|
|
|
|
|
|
|
|
|
Первые 13 строк программы осуществляют поиск, последние 11 строк—вставку. Время работы поисковой фазы равно (7C + C1 − 3S + 4)u, где
C = число произведенных сравнений;
Cl = число случаев, когда K KEY(P);
S = 1 при удачном поиске и 0 в противном случае: