Дональд Кнут. Искусство программирования. т.3 / knuth3
.pdf
302 Original pages: 572-600
Алгоритм Т. (Поиск по бору.) Алгоритм позволяет найти данный аргумент K в таблице записей, образующих M-арный бор.
Таблица 1
|
|
|
”Бор” для 31 наиболее употребительного английского слова |
|
|
|||||||
|
(1) |
(2) |
(3) |
(4) |
(5) |
(6) |
(7) |
(8) |
(9) |
(10) |
(11) |
(12) |
|
--- |
A |
--- |
--- |
--- |
I |
--- |
--- |
--- |
--- |
HE |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
A |
(2) |
--- |
--- |
--- |
(10) |
--- |
--- |
--- |
WAS |
--- |
--- |
THAT |
|
|
|
|
|
|
|
|
|
|
|
|
|
B |
(3) |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
C |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
D |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
HAD |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
E |
--- |
--- |
BE |
--- |
(11) |
--- |
--- |
--- |
--- |
--- |
--- |
THE |
|
|
|
|
|
|
|
|
|
|
|
|
|
F |
(4) |
--- |
--- |
--- |
--- |
--- |
OF |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
G |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
H |
(5) |
--- |
--- |
--- |
--- |
--- |
--- |
(12) |
WHICH |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
I |
(6) |
--- |
--- |
--- |
HIS |
--- |
--- |
--- |
WITH |
--- |
--- |
THIS |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
J |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
K |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
L |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
M |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
N |
NOT |
AND |
--- |
--- |
--- |
IN |
ON |
--- |
--- |
--- |
--- |
--- |
O |
(7) |
--- |
--- |
FOR |
--- |
--- |
--- |
TO |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
P |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
Q |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
R |
--- |
ARE |
--- |
FROM |
--- |
--- |
OR |
--- |
--- |
--- |
HER |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
S |
--- |
AS |
--- |
--- |
--- |
IS |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
T |
(8) |
AT |
--- |
--- |
--- |
IT |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
U |
--- |
--- |
BUT |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
V |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
HAVE |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
W |
(9) |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
X |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
Y |
YOU |
--- |
BY |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
Z |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
--- |
|
|
|
|
|
|
|
|
|
|
|
|
|
Узлы бора представляют собой векторы, индексы которых изменяются от 0 до M − 1; каждая компонента этих векторов есть либо ключ, либо ссылка (возможно, пустая).
Т1 [Начальная установка.) Установить указатель P так, чтобы он указывал на корень бора.
Т2 [Разветвление.] Занести в k следующую (стоящую правее) литеру аргумента K. (Если аргумент полностью обработан, мы засылаем в k ”пробел” или символ конца слова. Литера должна быть представлена целым числом в диапазоне 0 k < M.) Через X обозначим k-й элемент в узле NODE(P). Если X—ссылка, перейти на Т3; если X—ключ, то перейти на Т4.
Т3 [Продвижение.] Если X 6= , установить P X и вернуться на Т2; в противном случае алгоритм заканчивается неудачно.
Т4 [Сравнение.] Если K = X, алгоритм заканчивается удачно; в противном случае он заканчивается неудачно. 
Заметим, что, если поиск неудачен, все же будет найден элемент, лучше всего совпадающий с аргументом поиска. Это свойство полезно для некоторых приложений.
Чтобы сравнить время работы алгоритма и других алгоритмов данной главы, можно написать короткую программу для MIX, предполагая, что литера занимает один байт, а ключи— не более пяти байтов.
Original pages: 572-600 303
Программа Т. (Поиск по бору.) Предполагается, что все ключи занимают одно слово машины MIX; если ключ состоит менее чем из пяти литер, он дополняется справа пробелами. Так как мы используем для представления литер код MIX, каждый байт аргумента поиска должен содержать число, меньшее 30. Ссылки представлены как отрицательные числа в поле 0 : 2. Значения регистров: rI1 P, rX необработанная часть K.
START |
LDX |
К |
1 |
T1. Начальная установка. |
|
ENT1 |
ROOT |
1 |
P указатель на корень бора. |
2H |
SLAX |
1 |
C |
T2. Разветвление. |
|
STA |
*+1(2:2) C |
Выделение следующей литеры k. |
|
|
ENT2 |
0,1 |
C |
Q P + k. |
|
LD1N |
0,2(0:2) C |
P LINK(Q). |
|
|
J1P |
2B |
C |
TЗ. Продвижение. На T2, если P—ссылка 6= . |
|
LDA |
0,2 |
1 |
Т4. Сравнение. rA KEY(Q). |
|
CMPA |
K |
1 |
|
|
JE |
SUCCESS |
1 |
Удачный выход, если rA = k. |
FAILURE EQU |
* |
1 |
Выход, если K нет в бору. |
|
Время работы программы равно (8C + 8)u, где C—число проверяемых литер. Так как C 5, поиск не займет более 48 единиц времени.
Если теперь сравнить эффективность этой программы (использующей бор табл. 1) и программы 6.2.2Т (использующей оптимальное бинарное дерево поиска (рис. 13)), можно заметить следующее:
1.Бор занимает гораздо больше памяти: для представления 31 ключа использовано 360 слов,
вто время как бинарное дерево поиска занимает 62 слова памяти. (Однако упр. 4 показывает, что, проявив известную изобретательность, можно вместить бор табл. 1 в 49 слов.)
Picture: |
Рис. 31. Бор табл. 1, преобразованный в ”лес”. |
2.Удачный поиск в обоих случаях требует около 26 единиц времени. Но неудачный поиск оказывается более быстрым для бора и более медленным для бинарного дерева поиска. Для наших данных поиск чаще будет неудачным, нежели удачным, поэтому с точки зрения скорости бор более предпочтителен.
3.Если вместо 31 наиболее употребительного английского слова рассмотрим применение указателя KWIC (рис. 15), бор теряет свои преимущества вследствие характера данных. Например, бор требует 12 итераций, чтобы различить слова COMPUTATION и COMPUTATIONS. (В этом случае было бы лучше так построить бор, чтобы слова обрабатывались справа налево, а не слева направо.)
Первая публикация о боровой памяти принадлежит Рене де ла Брианде [Proc. Western Joint Computer Conf., 15 (1959), 295–298]. Он указал, что можно сэкономить память (за счет увеличения времени работы), если использовать связанный список для каждого узла-вектора, так как большинство элементов этого вектора обычно пусто. На самом деле предложенная идея ведет к замене бора табл. 1 на лес деревьев, изображенный на рис. 31. При поиске в таком лесу мы находим корень, соответствующий первой литере, затем сына этого корня, соответствующего второй литере, и т. д.
Фактически в своей статье де ла Брианде не прерывал разветвления так, как показано в табл. 1 или на рис. 31; вместо этого он представлял каждый ключ литера за литерой до достижения признака конца слова. Таким образом, в действительности де ла Брианде вместо дерева ”H” (рис. 31) использовал бы
Picture: |
Рис. стр. 576 |
Такое представление требует больше памяти, но делает обработку данных переменной длины особенно легкой. Если использовать на литеру два поля ссылок, можно легко организовать динамические вставки и удаления. Более того, во многих приложениях аргумент поиска поступает в ”распакованной” форме, т. е. каждая литера занимает целое слово, и такое дерево позволяет избежать ”упаковки” перед проведением поиска.
Если использовать обычный способ представления деревьев в бинарном виде, то (1) преобразуется в бинарное дерево (Для представления полного леса рис. 31 необходимо добавить указатель, ведущий из H направо, к соседнему корню I.) При поиске в этом бинарном дереве литеры аргумента сравниваются с литерами дерева; мы следуем по ссылкам RLINK, пока не найдем нужную литеру; затем берем ссылку LLINK и следующую литеру аргумента рассматриваем аналогичным образом.
304 Original pages: 572-600
Поиск в таком бинарном дереве в большей или меньшей степени сводится к сравнениям, причем разветвление происходит не по признаку ”меньше-больше”, а по ”равно-неравно”. Элементарная теория п. 6.2.1 гласит, что в среднем на то, чтобы различить N ключей, нужно по крайней мере log2 N сравнений; среднее число сравнений, производимых при поиске по дереву, подобному изображенному на рис. 31, не меньше числа проверок при использовании методов бинарного поиска, изложенных в x 6.2.
С другой стороны, бор табл. 1 позволяет сразу производить M-путевое разветвление; мы увидим, что при больших N поиск в среднем включает лишь около logM N = log2 N= log2 M итераций, если исходные данные—случайные числа. Мы увидим также, что применение схемы бора в ”чистом” виде (как в алгоритме T) требует порядка N= lnM узлов, если нужно различить N случайных ключей; следовательно, размер занимаемой памяти пропорционален MN= lnM.
Эти рассуждения показывают, что идея бора хороша лишь для небольшого числа верхних уровней дерева. Можно улучшить характеристики, комбинируя две стратегии: для нескольких первых литер использовать бор, а затем перейти на другую методику. Например, Э. Г. Сассенгат, мл. [Е. Н. Sassenguth, CACM, 6 (1963), 272–279] предложил использовать литерный анализ до достижения той части дерева, где остается, скажем, не более шести ключей, а затем произвести в этом коротком списке последовательный поиск. Далее мы увидим, что такая смешанная стратегия уменьшает количество узлов бора примерно в шесть раз без существенного изменения времени работы.
Приложение к английскому языку. Напрашивается много изменений основных стратегий бора и политерного поиска. Чтобы получить представление о некоторых из имеющихся возможностей, рассмотрим гипотетическое крупномасштабное приложение: предположим, что в памяти ЭВМ нужно запомнить довольно полный словарь английского языка. Для этого требуется достаточно большая внутренняя память, скажем на 50000 слов. Наша цель состоит в нахождении экономного способа представления словаря, обеспечивающего при этом достаточно быстрый поиск.
Реализация такого проекта—задача непростая. Требуется как хорошее знание содержания словаря, так и значительная программистская изобретательность. Давайте на некоторое время поставим себя в положение человека, приступающего к такому крупному проекту.
Обычный университетский словарь содержит свыше 100000 слов; наши запросы не столь велики, но рассмотрение подобного словаря позволяет наметить пути к решению. Если мы попытаемся применить боровую память, то вскоре заметим возможность важных упрощений. Предположим, что собственные имена и сокращения в расчет не принимаются. Тогда, если слово начинается на букву b, его вторая буква обязательно отлична от b, c, d, f, g, j, k, m, n, p, q, s, t, v, w, x, z. [Слово ”bdellium” (”бделлий”— род ароматической смолы) можно и не включать в словарь!] На самом деле, те же 17 букв не могут стоять на втором месте в словах, начинающихся с c, d, f, g, h, j, k, 1, m, n, p, r, t, v, w, x, z. Исключение составляют несколько слов, начинающихся с ct, cz, dw, fj, gn, mn, nt, pf, pn, pt, tc, tm, ts, tz, zw, и заметное количество слов, начинающихся с kn, ps, tw. Один из способов использовать этот факт состоит в кодировании букв (например, можно иметь таблицу из 26 слов и выполнять команду вроде ”LD1 TABLE,1”) таким образом, чтобы согласные b, c, d, : : : , z переводились в специальное представление, большее, чем числовой код оставшихся букв a, e, h, i, l, o, r, u, y. Теперь многие узлы бора могут быть укорочены до 9-путевого разветвления; специальная ячейка выхода используется для редко встречающихся букв. Такой подход позволяет сэкономить память во многих частях бора, предназначенного для английского языка, а не только при обработке второй буквы.
В обычном университетском словаре слова начинаются лишь с 309 из 262 = 676 возможных комбинаций двух букв, а из этих 309 пар 88 служат началом 15 или менее слов. (Типичные примеры таких пар: aa, ah, aj, ak, ao, aq, ay, az, bd, bh, : : : , xr, yc, yi, yp, yt, yu, yw, za, zu, zw; большинство людей может назвать максимум по одному слову из большинства этих категорий.) Если встречается одна из указанных редких категорий, можно от боровой памяти перейти к некоторой другой схеме, например к последовательному поиску.
Другим способом уменьшения требуемого для словаря объема памяти является использование приставок. Например, если ищется слово, начинающееся с re-, pre-, anti-, trans-, dis-, un- и т. д., можно отделить приставку и искать остаток слова. Таким путем можно удалить много слов типа reapply (повторно применять), recompute (снова вычислить), redecorate (заново декорировать), redesign (перепроектировать), redeposit (перекладывать) и т. д.; однако нужно оставить слова вроде remainder (остаток), requirement (требование), retain (сохранять), remove (удалять), readily (охотно) и т. д., так как их значения без приставки ”re-” покрыты тайной. Итак, сначала следует искать слово и лишь затем пытаться удалить приставку, если первый поиск оказался неудачным.
Еще важнее использовать суффиксы. Не вызывает сомнений, что двукратное включение каждого существительного и глагола в единственном и множественном числе было бы слишком расточительным; имеются и другие типы суффиксов. Например, следующие окончания можно добавлять ко
Original pages: 572-600 305
многим глагольным основам, получая набор родственных слов:
-e |
-es |
-ing |
-ed |
-s |
-ings |
-edlv |
-able |
-ingly |
-er |
-ible |
-ion |
-or |
-ably |
-ions |
-ers |
-ibly |
-ional |
-ors |
-ability |
-ionally |
|
-abilities |
|
(Многие из этих суффиксов сами составлены из суффиксов.) Если попытаться добавить эти суффиксы
к основам
computcalculatsearchsuffixtranslatinterpretconfus-
мы увидим, как много слов получается; это чрезвычайно увеличивает вместимость нашего словаря. Безусловно, при такой процедуре получается и много несуществующих слов, например ”compution”; основа computatоказывается столь же необходимой, как и comput-. Однако вреда в этом нет, поскольку такие комбинации и не будут служить аргументами поиска, а если бы некий автор придумал бы слово ”computedly”, мы имели бы для него готовый перевод. Заметим, что большинству людей понятно слово ”confusability” (смущаемость), хотя оно встречается в немногих словарях; наш словарь будет давать подходящее толкование. При правильной обработке приставок и суффиксов наш словарь сможет из одной лишь глагольной основы ”establish” вывести значение слова ”antidisestablishmentarianism”.
Разумеется, нужно заботиться о том, чтобы значение слова полностью определялось основой
исуффиксом; исключения должны быть внесены в словарь таким образом, чтобы их можно было найти до обращения к поиску суффикса. Например, сходство между словами ”socialism” (социализм)
и”socialist” (социалист) способно только сбить с толку при анализе слов ”organism” (организм) и ”organist” (органист)!
Можно использовать целый ряд приемов для уменьшения объема памяти. Но как в единой системе компактно представить разнообразие методов? Ответ состоит в том, чтобы трактовать словарь как программу, написанную на специальном машинном языке для специальной интерпретирующей системы (ср. с п. 1.43); записи в узле бора можно трактовать как инструкции. Например, в табл. 1 мы имеем два вида ”инструкций”, а программа T использует знаковый бит как код операции; знак минус означает ответвление к другому узлу и переход к следующей литере за следующей инструкцией, в то время как знак плюс вызывает сравнение аргумента с определенным ключом.
В интерпретирующей системе для нашего словаря мы могли бы иметь операции следующих типов:
Проверка n, , . ”Если следующая литера аргумента кодируется числом k n, перейти в ячейку + k за следующей инструкцией; в противном случае перейти в ячейку ”.
Сравнение n, , . ”Сравнить оставшиеся литеры аргумента с n словами, расположенными в ячейках , +1, : : : , +n −1. Если подходящее слово имеет адрес +k, поиск кончается удачно и ”значение” лежит в ячейке + k, но если подходящего слова нет, поиск кончается неудачно”.
Расщепление , . ”Слово, обработанное до данного места, возможно, является приставкой или основой. В продолжение поиска нужно направиться в ячейку за следующей инструкцией. Если этот поиск неудачен, необходимо продолжить поиск, рассматривая оставшиеся литеры аргумента как новый аргумент. Если этот поиск удачен, скомбинировать найденное ”значение” со ”значением” ”.
Операция проверки реализует концепцию поиска по бору, а операция сравнения обозначает переход к последовательному поиску. Операция расщепления служит для обработки приставок и суффиксов. Можно, основываясь на более детальном изучении специфических черт английского языка, предложить некоторые другие операции. Дальнейшая экономия памяти достигается за счет исключения параметра из каждой инструкции, так как память может быть организована так, чтобы вычислялось с помощью , n или адреса инструкции.
306 Original pages: 572-600
Более полная информация об организации словаря имеется в интересных статьях Сиднея Лэмба и Уильяма Якобсена, мл. [Mechanical Translation, 6 (1961), 76–107]; Ю. С. Шварца [JACM, 10 (1963), 413–439]; Галли и Ямады [IBM Systems J., 6 (1967), 192–207].
Бинарный случай. Рассмотрим теперь частный случай M = 2, когда в каждый момент времени обрабатывается один бит аргумента поиска. Разработаны два интересных метода, особенно подходящие для данного случая.
Первый метод, который мы будем называть цифровым поиском по дереву, предложен Коффманом и Ивом [CACM, 13 (1970), 427–432, 436]. Так же как и в алгоритмах поиска по дереву из п. 6.2.2, в узлах дерева хранятся полные ключи, но для выбора левой или правой ветви используются не результат сравнения ключей, а биты аргумента. На рис. 32 изображено дерево, построенное этим методом; оно составлено из 31 наиболее употребительного английского слова, причем слова вставлялись в порядке уменьшения частот. Чтобы получить бинарные данные для этой иллюстрации, слова записывались
влитерном коде машины MIX с последующим преобразованием в двоичные числа, содержащие по 5 битов в байте. Таким образом, слово WHICH превратится в ”11010 01000 01001 00011 01000”.
Чтобы найти WHICH на рис. 32, мы сначала сравниваем его со словом THE в корне дерева. Так как они не совпадают и первый бит слова WHICH равен 1, нужно идти вправо и произвести сравнение со словом OF. Так как совпадения не произошло и второй бит WHICH равен 1, мы идем вправо и сравниваем наш аргумент со словом WITH и т. д.
Дерево рис. 32 построено способом, похожим на способ построения дерева рис. 12 из п. 6.2.2, лишь для разветвления вместо сравнений использовались биты ключей, поэтому интересно отметить разницу между ними. Если рассмотреть заданные частоты, цифровой поиск по дереву рис. 32 требует
всреднем 3:42 сравнения на удачный поиск; это несколько лучше, чем 4:04 сравнения, требующегося для рис. 12, хотя, разумеется, время самого сравнения будет различным в двух этих случаях.
|
Picture: |
Рис. 32. Дерево цифрового поиска для 31 наиболее употребительного англий- |
||||||
|
|
ского слова, слова вставлены в порядке уменьшения частот. |
|
|||||
|
Алгоритм D. (Цифровой поиск по дереву.) Этот алгоритм позволяет найти данный аргумент K в |
|||||||
таблице записей, образующих описанным выше способом бинарное дерево. Если K нет в таблице, в |
||||||||
дерево в подходящем месте вставляется новый узел, содержащий K. |
|
|
||||||
|
Как и в алгоритме 6.2.2Т, здесь предполагается, что дерево непусто, а его узлы содержат поляKEY, |
|||||||
LLINK и RLINK. Читатель увидит, что эти алгоритмы почти идентичны. |
|
|
||||||
D1 |
[Начальная установка.] Установить P ROOT, K1 |
K. |
|
|
||||
D2 |
[Сравнение.] Если K = KEY(P), поиск оканчивается удачно; в противном случае занести в b самый |
|||||||
|
левый бит K1 и сдвинуть K1 на единицу влево (т. е. удалить этот бит и добавить справа 0). Если |
|||||||
|
b = 0, перейти на D3; в противном случае перейти на D4. |
|
|
|||||
D3 |
[Шаг влево.] Если LLINK(P) 6= , установить P |
LLINK(P) и вернуться на D2; в противном случае |
||||||
|
перейти на D5. |
|
|
|
|
|
|
|
D4 |
[Шаг вправо.] Если RLINK(P) 6= , установить P |
RLINK(P) и вернуться на D2. |
|
|||||
D5 |
[Вставка в дерево.] Выполнить Q ( AVAIL, KEY(Q) |
K, LLINK(Q) |
RLINK(Q) |
. Если b = 0, |
||||
|
установить LLINK(P) Q; в противном случае RLINK(P) Q. |
|
|
|
|
|||
|
Алгоритм 6.2.2T по сути своей бинарный, однако приведенный алгоритм, как нетрудно заметить, |
|||||||
можно легко распространить на M-арный цифровой поиск для любого M 2 (см. упр. 13). |
||||||||
|
Picture: |
Рис. 33. Пример текста и дерева Патриции. |
|
|
||||
Дональд Моррисон [JACM, 15 (1968), 514–534] придумал очень привлекательный способ формирования N-узловых деревьев поиска, основанный на бинарном представлении ключей, без запоминания ключей в узлах. Его метод, названный ”Патриция” (Практический алгоритм для выборки информации, закодированной буквами и цифрами)7, особенно подходит для работы с очень длинными ключами переменной длины, такими, как заголовки или фразы, хранящиеся в файле большого объема.
Основная идея Патриции состоит в том, чтобы строить бинарное дерево, избегая однопутевых разветвлений, для чего в каждый узел включается число, показывающее, сколько битов нужно пропустить, перед тем как делать следующую проверку. Существует несколько способов реализации этой идеи; возможно, простейший с точки зрения объяснения изображен на рис. 33. Имеется массив
7 В оригинале ”Patricia”—Practical Algorithm To Retrieve Information Coded In Alphanumeric.—
Прим. перев.
Original pages: 572-600 307
битов TEXT (обычно довольно длинный); он может храниться во внешнем файле с прямым доступом, так как при каждом поиске обращение к нему происходит лишь раз. Каждый ключ, который должен храниться в нашей таблице, определяется местом начала в тексте; можно считать, что он идет от места начала до конца текста. (Патриция не ищет точного равенства между ключом и аргументом, а определяет, существует ли ключ, начинающийся с аргумента.)
Впримере, изображенном на рис. 33, имеется семь ключей, начинающихся с каждого входящего
втекст слова, а именно ”THIS IS THE HOUSE THAT JACK BUILT.”, ”IS THE HOUSE THAT JACK BUILT.”,
: : : , ”BUILT.”. Налагается важное ограничение, состоящее в том, что один ключ не может служить началом другого; это условие выполняется, если мы оканчиваем текст специальным кодом конца текста (в данном случае ”.”), который нигде больше не появляется. То же ограничение подразумевается
всхеме бора алгоритма T, где признаком конца служил ” ”.
Дерево, используемое Патрицией для поиска, должно целиком помещаться в оперативной памяти, или его нужно организовать по страничной схеме, описанной в п. 6.2.4. Оно состоит из головы и N − 1 узлов, причем узлы содержат несколько полей:
KEY—указатель на текст. Это поле должно иметь длину не менее log2 C битов, где C—число литер в тексте. Слова, изображенные на рис. 33 внутри узлов, на самом деле должны быть представлены указателями на текст; например, вместо ”(JACK)” узел содержит число 24 (начальную точку ключа ”JACK BUILT.”).
LLINK и RLINK—ссылки внутри дерева. Они должны иметь длину не менее log2 N битов. LTAG и RTAG—однобитовые поля, по которым мы можем судить, являются ли LLINK и RLINK соответственно ссылками на сыновей или на предков. Пунктирные линии на рис. 33 соответствуют ссылкам, для которых TAG = 1.
SKIP—число битов, которые нужно пропустить при поиске, как объяснялось выше. Это поле должно быть достаточно большим для того, чтобы вместить наибольшее число k, для которого в двух различных ключах найдутся совпадающие подцепочки из k битов; обычно для практических целей достаточно считать, что k не слишком велико, и при превышении размеров поля SKIP выдавать сообщение об ошибке. На рис. 33 поля SKIP изображены внутри каждого узла числами.
Голова содержит лишь поля KEY, LLINK и LTAG.
Поиск в дереве Патриции выполняется следующим образом: предположим, мы ищем слово THAT (бинарное представление 10111 01000 00001 10111). По содержимому поля SKIP в корневом узле мы заключаем, что сначала нужно проверить первый бит аргумента. Он равен 1, поэтому надо двигаться вправо. Поле SKIP следующего узла побуждает нас проверить 1+11 = 12-й бит аргумента. Он равен 0, и мы идем влево. Изучив поле SKIP следующего узла, видим, что нужно проверить 12 + 1 = 13-й бит, который равен 0. Так как LTAG = 1, мы идем на узел γ, который отсылает нас к массиву TEXT. По тому же пути пойдет поиск для любого аргумента, имеющего бинарное представление 1xxxx xxxxx x00 : : :, поэтому нужно сравнить аргумент с найденным ключом.
Предположим, с другой стороны, что мы ищем некоторый ключ, начинающийся с TH, или все такие ключи. Поиск начинается, как и раньше, но в конце концов мы обратимся к (несуществующему) 12-му биту десятибитового аргумента. Теперь нужно сравнить аргумент с фрагментом массива TEXT, который специфицируется в текущем узле (в данном случае γ); если совпадения не произошло, то не существует ключа, началом которого служит аргумент поиска; если совпадение имело место, аргумент является началом любого ключа, на который указывают пунктирные линии, выходящие из узла γ и его потомков.
Более точно этот процесс описывается следующим образом.
Алгоритм P. (Патриция.) Алгоритм, используя массив TEXT и дерево с описанными выше полями KEY, LLINK, RLINK, LTAG, RTAG и SKIP в узлах, позволяет определить, существует ли в массиве TEXT ключ, начинающийся с аргумента K. (Если имеется r 1 таких ключей, можно за O(r) шагов определить расположение каждого из них; см. упр. 14.) Предполагается, что есть по крайней мере
один ключ. |
|
|
P1 |
[Начальная установка.] Установить P HEAD и j 0 (Переменная P является указателем, кото- |
|
|
рый будет двигаться вниз по дереву, а j—это счетчик, определяющий позиции битов аргумента.) |
|
|
Занести в n количество битов аргумента K. |
|
P2 |
[Шаг влево.] Установить Q P и P LLINK(Q). Если LTAG(Q) = 1, перейти на P6. |
|
P3 |
[Пропуск битов.] (В этот момент мы знаем, что, если первые j битов аргумента соответствуют |
|
|
некоторому ключу, они соответствуют ключу, начинающемуся в KEY(P).) Установить j |
j + |
|
SKIP(P). Если j > n, перейти на P6. |
|
|
|
|
|
Original pages: 572-600 309 |
Аналогично, если через CN обозначить среднее суммарное количество проверок битов, нужных для |
||||
поиска в бору всех N ключей, то C0 = C1 = 0, а |
|
|
|
|
CN = N + M1−N k |
N |
(M − 1)N−kCk; |
N 2: |
(5) |
k |
||||
X |
|
|
|
|
В упр. 17 показано, как работать с рекуррентными соотношениями такого вида, а в упр. 18–25 разрабатывается соответствующая теория случайных боров. [С другой точки зрения к анализу AN впервые подошли Л. Р. Джонсон и М. Мак-Эндрю [IBM J. Res. and Devel., 8 (1964), 189–193] в связи с эквивалентным аппаратно-ориентированным алгоритмом сортировки.]
Переходя теперь к изучению деревьев цифрового поиска, мы обнаруживаем, что, с одной стороны, формулы похожи, а с другой—достаточно различны, и сразу не ясно, как исследовать асимпто-
|
|
|
|
|
|
тическое поведение. Например, если CN обозначает среднее суммарное количество проверок битов, |
|||||
производимых при поиске всех N ключей в бинарном дереве цифрового поиска, нетрудно вывести, |
|||||
|
|
|
|
|
|
как и раньше, что C0 |
= C1 = 0 и |
X |
|
|
|
|
|
N |
|
|
|
|
CN+1 = N + M1−N |
k |
k (M − 1)N−kCk; |
N 0: |
(6) |
Это почти идентично выражению (5), но появления N + 1 вместо N в левой, части уравнения достаточно для того, чтобы изменить общий характер рекуррентности, так что методы, используемые для изучения (5), в данном случае непригодны.
Рассмотрим сначала бинарный случай. На рис. 35 изображено дерево цифрового поиска, соответствующее 16 ключам рис. 34, вставленным в порядке, использованном в примерах из гл. 5. Среднее число проверок битов при случайном удачном поиске найти легко—оно равно длине внутреннего пути дерева, деленной на N, так как нужно l проверок, чтобы найти узел, расположенный на уровне l. Заметим, однако, что среднее число проверок битов при случайном неудачном поиске не так просто связано с длиной внешнего пути дерева, ибо неудачный поиск с большей вероятностью оканчивается вблизи корня; например, вероятность достижения левой ветви узла 0075 (рис. 35) равна 1=8 (рассматриваются бесконечно точные ключи), а в левую ветвь узла 0232 мы попадаем с вероятностью, равной лишь 1=32. (По этой причине при равномерном распределении ключей деревья цифрового поиска, как правило, лучше сбалансированы, чем бинарные деревья поиска, использовавшиеся в алгоритме 6.2.2T.)
Для описания соответствующих характеристик деревьев цифрового поиска можно использовать производящую функцию. Пусть на уровне l расположено al узлов; рассмотрим производящую функцию a(z) = Palzl. Например, рис. 35 соответствует
Picture: |
Рис. 35 Случайное дерево цифрового поиска, построенное с помощью алго- |
|
|||
|
ритма D. |
|
|
|
|
функция a z = 1 + 2z + 4z2 + 5z3 + 4z4. Если b |
|
узлов уровня l являются внешними и b(z) = |
b |
zl, то |
|
в силу упр.(6.2.1) |
-25 имеем |
l |
|
P l |
|
|
b(z) = 1 + (2z − 1)a(z): |
|
(7) |
||
Например, 1 + (2z − 1)(1 + 2z + 4z2 + 5z3 + 4z4) = 3z3 + 6z4 + 8z5. Среднее число проверок битов при случайном удачном поиске равно a0(1)=a(l), так как a0(1) есть длина внутреннего пути дерева, а a(1)—
количество внутренних узлов. Среднее число проверок битов для случайного неудачного поиска рав- |
||||||||
P |
lbl2−l = 21b0 |
|
21 |
|
|
21 |
|
9 |
но |
|
|
= a |
|
, так как мы достигаем данного внешнего узла уровня l с вероятностью 2−l. |
|||
При неудачном поиске число сравнений совпадает с числом проверок битов, а при ”удачном”—на единицу больше этого числа. Для дерева рис. 35 удачный поиск в среднем требует 216 проверок битов и 3169 сравнений; в процессе неудачного поиска производится 387 сравнений и проверок (в среднем).
Введем теперь ”усредненную” по всем деревьям с N узлами функцию a(z) и обозначим ее че-
P
рез gN(z). Иными словами, gN(z) = pT aT (z), где сумма берется по всем бинарным деревьям цифрового поиска T с N внутренними узлами; aT (z) есть производящая функция для внутренних узлов T,
а pT есть вероятность того, что при вставке N случайных чисел с помощью алгоритма D получается
дерево T. Таким образом, для удачного поиска среднее число проверок битов равно g0 (1)=N, а для
N
неудачного—gN 12 .
Имитируя процесс построения дерева, gN(z) можно вычислить следующим образом. Если a(z) есть производящая функция для дерева с N узлами, можно образовать N + 1 деревьев, делая вставку в позицию любого внешнего узла. Эта вставка производится в данный внешний узел уровня l с вероятностью 2−l; следовательно, сумма производящих функций для N + 1 новых деревьев, умноженных


