Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
55
Добавлен:
01.05.2014
Размер:
2.53 Mб
Скачать

гут иметь разных родителей). Сделать это предлагается заменой поддерева с корнем в нарушенном узле, чей родитель имеет меньший ключ, на поддерево с корнем в i-ранговом брате нарушаемого узла, чей родитель имеет больший ключ. Легко проверить, что такая замена не приводит к созданию новых нарушений. Пусть узел y общий родитель двух нарушаемых узлов после замены принадлежит дереву F.

Разобьем дальнейшее рассмотрение на два случая.

1.Ранг y равен i + 1. Пусть F 1 и F 2 – это толстые деревья ранга i с корнями в двух нарушаемых узлах, а дерево F у толстое дерево

ранга i, полученное из поддерева с корнем в узле y удалением поддеревьев F 1 и F 2.

а) Если узел y не является корнем дерева F, то удаляем из дерева F поддерево F у. Из трех толстых деревьев (F, F 1, F 2) ранга i об-

разуем одно дерево ранга i + 1, чей корень z является узлом с наименьшим ключом среди корней деревьев F, F 1, F 2. Вставляем в дерево F вновь полученное толстое дерево с корнем в узле z вместо поддерева с корнем в узле у. Если узел z оказывается

нарушенным, инкрементируем di+1. Значение i-го разряда делаем нулевым.

б) Если узел y – корень дерева F, то удаляем дерево F из кучи. Из трех толстых деревьев (F, F 1, F 2) ранга i образуем одно дерево

ранга i, чей корень z является узлом с наименьшим ключом среди ключей корней деревьев F, F 1, F 2. Вставляем вновь полученное толстое дерево с корнем в узле z в кучу. Значение i-го разряда делаем нулевым.

2.Если ранг y больше, чем i + 1, то, по условию регулярности счетчика нарушений, узел y должен иметь хотя бы одного сына w ранга i + 1, который не является (i + 1)-ранговым нарушением, и два i-ранговых сына w должны быть также ненарушенными. Тогда заменяем два нарушенных i-ранговых сына узла y на два хороших i-ранговых сына узла w. Тем самым мы свели задачу к

случаю 1.

Можно доказать, что рассматриваемая операция не испортит регулярности счетчика.

Инкрементирование i-го разряда счетчика нарушений (IncCount Violation (i, p)). Используя описанную выше операцию фиксации, можно осуществить инкрементирование i-го разряда счетчика нарушений следующими операторами.

281

FixCountViolation (i);

FixCountViolation (CountViolation [i]^.ForwardPointer);

InsertViolation(i, pNode);

CountViolation[i].Value:= CountViolation[i].Value + 1;

FixCountViolation (i);

FixCountViolation (CountViolation [i]^.ForwardPointer);

Трудоемкость операции O(1).

Удаление нарушения из кучи. Заметим, что удаление нарушения из кучи подразумевает наличие в куче этого нарушения; пусть это нарушение ранга i. Тогда значение i-го разряда для счетчика нарушений не равно нулю. Следовательно, уменьшение этого значения на единицу не испортит регулярности и не потребует обновления каких-либо указателей. Необходимо лишь уменьшить на единицу значение переменной CountViolation [i].Value и обработать указатели FirstViolation и SecondViolation.

Очевидно, что трудоемкость этой операции O(1).

Нахождение узла с минимальным значением ключа среди всех на-

рушений. Для реализации этой функции предлагается перебрать все нарушения до максимального ранга и найти среди них узел с минимальным весом. Трудоемкость данной операции O(log n).

Основные операции:

Операция make-heap заключается в инициализации счетчиков; трудоемкость O(1).

Операция FindMin возвращает указатель на минимальный элемент. Трудоемкость O(1).

Операция Insert(key). Чтобы выполнить эту операцию, делаем новый элемент отдельным деревом и выполняем процедуру вставки нового элемента ранга 0 в корневой счетчик. После этого, если необходимо, корректируем значение указателя на минимальный элемент.

Операция уменьшения ключа DecreaseKey(, p). Чтобы выполнить эту операцию, поступим следующим образом. Пусть x – узел, на который указывает указатель p. Вычитаем из ключа узла х. Если новый ключ х меньше минимального ключа кучи H, обмениваем ключ элемента p с ключом минимального элемента. Новых нарушений операция не создаст. Пусть r – ранг x. Если x – нарушаемый узел, добавляем x как новое r-ранговое нарушение инкрементированием r цифры dr счетчика нарушений. Трудоемкость O(1).

Операция DeleteMin выполняется следующим образом. Удаляем под-

282

дерево с корнем в минимальном узле из леса. Минимальность этого элемента гарантирует нам, что среди его детей нарушений порядка кучи не было. То есть нет необходимости работать со счетчиком нарушений. Затем вставляем в кучу все деревья с корнями, расположенными в детях удаляемого узла. Очевидно, что новый минимальный ключ – либо в корне дерева леса, либо в нарушенном узле. Выполняем поиск нового минимального элемента среди корней деревьев и нарушенных узлов.

Если минимальный элемент оказался в нарушенном узле, то обмениваем его с элементом, хранимым в корне этого дерева, корректируя корневой счетчик, если это необходимо. После замены новый минимум – в корне дерева леса. Этот корень будет новым минимальным узлом. Трудоемкость операции равна О(log n).

Операция удаления элемента. Выполняется с помощью DecreaseKey

и затем DeleteMin. Трудоемкость операции О(log n).

Операция Meld(h1, h2). Выполняется следующим образом. Первый шаг – фиксируются все нарушения в куче с меньшим максимальным рангом (разрывая связь произвольно). Не уменьшая общности, считаем, что эта куча – h2. Пройти по счетчику нарушений h2 от младшей цифры к старшей, пропуская цифры со значением 0. Для i-й цифры di 0 делаем операцию фиксирования на каждой цифре, показываемой прямым указателем di, если эта цифра имеет значение 2. Затем, если di = 2, фиксируем di. Если di = 1, преобразуем это i-ранговое нарушение в (i + 1)-ранговое нарушение, как при фиксировании, используя i-рангового брата нарушенного узла вместо (несуществующего) другого i-рангового нарушения.

Как только h2 не будет содержать каких-либо нарушений, вставить корни из корневого счетчика h2 в корневой счетчик h1 инкрементированием соответствующих цифр. Если минимальный узел h2 содержит меньший ключ, чем минимальный узел h1, установить новым минимальным узлом h1 минимальный узел h2. Вернуть модифицированную кучу h1 в качестве результата Meld. Трудоемкость операции равна О(log n).

Операция DeleteViolation. Для освобождения кучи от нарушений достаточно выполнить операторы

for i:= 0 to h2^.MaxRank do

if (CountViolation[i].Value = 2) then FixCountViolation( i);

for i:= 0 to h2^.MaxRank do if (CountViolation[i].Value = 1) then {IncCountViolation(i, SearchBrother (CountViolation[i].FirstViolation)); FixCountViolation(i)};

283

Основываясь на описанной выше реализации толстой кучи получаем следующий результат. В толстых кучах операции FindMin, Insert и DecreaseKey выполняются за время O(1), а Delete, DeleteMin и Meld – за время O(log n).

Замечания. Существует альтернативное представление избыточных счетчиков. Вместо одной записи на цифру можно использовать одну запись на блок одинаковых цифр. Инкрементирование любой цифры можно выполнить за время O(1), используя это альтернативное представление. Преимущество такого представления – возможность расширить счетчик на произвольное число одинаковых цифр за постоянное время.

Г. Бродал описывает кучевидную структуру, которая теоретически лучше, чем толстые кучи, так как их временная оценка для Meld O(1) в худшем случае. Структура Бродала, однако, намного сложнее толстых куч.

284

Сводные сведения о трудоемкости операций с приоритетными очередями

1. Трудоемкость операций над различными реализациями приоритетной очереди в худшем случае

 

 

Левосто-

Ленивая

Самоор-

Биноми-

Ленивая

Фибонач-

Слаборас-

Куча

 

d-куча

левосто-

ганизую-

биноми-

тущая ку-

Операции

ронняя

ронняя

щаяся

альная

альная

чиева куча

ча (run-

Бродала

 

 

куча

куча

куча

очередь

очередь

 

relaxed)

 

ВСТАВИТЬ

O(logd n)

O(log n)

O(1)

O(n)

O(log n)

O(1)

O(1)

O(1)

O(1)

 

 

 

 

 

 

 

 

 

 

МИН

O(1)

O(1)

O(F)*

O(1)

O(1)

O(1)

O(1)

O(1)

O(1)

 

 

 

 

 

 

 

 

 

 

УДАЛИТЬ

O(d logd n)

O(log n)

O(F)*

O(n)

O(log n)

O(n)

. . .

O(log n)

O(log n)

МИН

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

УДАЛИТЬ

O(d logd n)

O(log n)

O(1)

. . .

O(log n)

. . .

. . .

O(log n)

O(log n)

 

 

 

 

 

 

 

 

 

 

УМЕНЬШИТЬ

O(logd n)

O(log n)

O(1)

. . .

O(log n)

. . .

. . .

O(1)

O(1)

КЛЮЧ

 

 

 

 

 

 

 

 

 

СЛИТЬ

O(log n)

O(1)

O(n)

O(log n)

O(1)

 

O(log n)

O(1)

 

 

 

 

 

 

 

 

 

 

ОБРАЗОВАТЬ

O(n)

O(n)

O(n)

. . .

O(n)

 

 

O(1)

O(1)

ОЧЕРЕДЬ

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

* F = k max {1, log (n/(k + 1))}

285

286

2. Амортизационная трудоемкость выполнения операций

 

 

Левосто-

Ленивая

Самоор-

Биноми-

Ленивая

Фибонач-

Тонкая

Толстая

Операции

d-куча

левосто-

ганизую-

биноми-

ронняя

ронняя

щаяся

альная

альная

чиева

куча (thin

куча

 

 

куча

куча

куча

очередь

очередь

куча

heap)

(fat heap)

 

 

 

 

 

 

 

 

 

 

ВСТАВИТЬ

O(logd n)

O(log n)

O(1)

O(1)

O(log n)

O(1)

O(1)

O(1)

O(1)

 

 

 

 

 

 

 

 

 

 

МИН

O(1)

O(1)

. . .

O(1)

O(1)

O(1)

O(1)

O(1)

O(1)

 

 

 

 

 

 

 

 

 

 

УДАЛИТЬ

O(d logd n)

O(log n)

. . .

O(log n)

O(log n)

O(log n)

O(log n)

O(log n)

O(log n)

МИН

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

УДАЛИТЬ

O(d logd n)

O(log n)

O(1)

. . .

O(log n)

. . .

O(log n)

O(log n)

O(log n)

 

 

 

 

 

 

 

 

 

 

УМЕНЬШИТЬ

O(logd n)

O(log n)

O(1)

. . .

O(log n)

. . .

O(1)

O(1)

O(1)

КЛЮЧ

 

 

 

 

 

 

 

 

 

СЛИТЬ

O(log n)

O(1)

O(log n)

O(log n)

O(1)

O(1)

O(1)

O(log n)

 

 

 

 

 

 

 

 

 

 

ОБРАЗОВАТЬ

O(n)

O(n)

O(n)

. . .

O(n)

 

O(1)

O(1)

O(1)

ОЧЕРЕДЬ

 

 

 

 

 

 

 

 

 

Глава 5. ПОИСКОВЫЕ ДЕРЕВЬЯ

5.1. Двоичные деревья поиска

Общие сведения. Деревья поиска предназначены для представления словарей как абстрактного типа данных. Так же как и приоритетные очереди, они представляют взвешенные множества, но с другим набором операций, а именно:

Search – поиск элемента с заданным ключом;

Minimum – поиск элемента с минимальным ключом;

Maximum – поиск элемента с максимальным ключом;

Predecessor – поиск элемента с предыдущим ключом;

Successor – поиск элемента со следующим ключом;

Insert – вставка элемента со своим ключом;

Delete – удаление указанного элемента.

Считается, что каждый элемент словаря имеет ключ (вес), принимающий значение из какого-либо линейно упорядоченного множества. Таким множеством может быть, например, числовое множество или множество слов в некотором алфавите. В последнем случае в качестве линейного порядка можно рассматривать лексикографический порядок. Таким образом, дерево поиска может быть использовано и как словарь, и как приоритетная очередь.

Время выполнения основных операций пропорционально высоте дерева. Если каждый внутренний узел двоичного дерева имеет ровно двух потомков, то его высота и время выполнения основных операций пропорциональны логарифму числа узлов. Напротив, если дерево представляет собой линейную цепочку из n узлов, это время вырастает до Θ(n). Известно, что высота случайного двоичного дерева поиска есть Ο(lоg n), так что в этом случае время выполнения основных операций есть Θ(lоg n).

Конечно, возникающие на практике двоичные деревья поиска могут быть далеки от случайных. Однако, приняв специальные меры по балансировке деревьев, мы можем гарантировать, что высота деревьев с n узлами будет Ο (lоg n). Ниже рассмотрим один из подходов такого рода (красно-черные деревья и как частный случай АВЛ-деревья). Будут рассмотрены также Б-деревья, которые особенно удобны для данных, хранящихся во вторичной памяти с произвольным доступом (на диске).

287

Представление двоичных деревьев поиска. Двоичным деревом по-

иска называется корневое двоичное дерево, каждому узлу которого поставлен в соответствие взвешенный элемент. При этом для каждого узла x выполняется следующее условие:

Веса всех узлов левого поддерева в дереве с корнем x меньше, а веса узлов его правого поддерева больше веса узла x или равны ему.

Представляется такое дерево узлами следующего вида

Node = (element, key, left, right, parent).

Доступ к дереву T осуществляется с помощью ссылки root. Процедура Walk (x) обходит все узлы поддерева с корнем в узле x и

печатает их ключи в неубывающем порядке.

pocedure Walk (x); begin

if (x nil) then {Walk (left[x]); write (key[x]); Walk (right[x])} end;

Свойство упорядоченности гарантирует правильность алгоритма. Время работы на дереве с n вершинами есть Θ(n), каждая вершина обрабатывается один раз. Оператор Walk(root) напечатает ключи всех элементов в неубывающем порядке.

Заметим, что порядок, при котором корень предшествует узлам обоих поддеревьев, называется preorder; порядок, в котором корень следует за ними, называется postorder.

Упражнения

1.Нарисуйте двоичные деревья поиска высоты 2, 3, 4, 5 и 6 для одного

итого же множества ключей 1, 4, 5, 10, 16, 17, 21.

2.Напишите нерекурсивный алгоритм, печатающий ключи в двоичном дереве поиска в неубывающем порядке.

3.Напишите рекурсивные алгоритмы для обхода деревьев в различных порядках (preorder, postorder). Как и раньше, время работы должно

быть O(n) (где n – число вершин).

4. Покажите, что любой алгоритм построения двоичного дерева поиска, содержащего заданные n элементов, требует времени (n log n). Воспользуйтесь тем, что сортировка n чисел требует (n log n) действий.

Операции с двоичным поисковым деревом. Покажем, что двоичные поисковые деревья позволяют выполнять операции Search, Minimum,

288

Maximum, Successor и Predecessor за время Ο(h), где h – высота дерева.

Поиск (Search). Процедура поиска получает на вход искомый ключ k и указатель x на корень поддерева, в котором производится поиск. Она возвращает указатель на вершину с ключом k (если такая есть) или nil (если такой вершины нет).

procedure Search (x, k); begin

if (x = nil) or (k = key [x]) then return x;

if (k < key [x]) then return Search (left[x], k) else return Search (right[x], k)

end;

В процессе поиска мы двигаемся от корня, сравнивая ключ k с ключом, хранящимся в текущей вершине x. Если они равны, поиск завершается. Если k < key[x], то поиск продолжается в левом поддереве x. Если k > key[x], то поиск продолжается в правом поддереве. Длина пути поиска не превосходит высоты дерева, поэтому время поиска есть O(h) (где h – высота дерева).

Итеративная версия процедуры Поиск

procedure IterativeSearch (x, k); begin

while (x nil) and (k key [x]) do

if k < key [x] then x:= left[x] else x:= right[x]; return (x)

end;

Минимум и Максимум. Элемент с минимальным ключом в дереве поиска можно найти, пройдя от корня по указателям left пока не упремся в nil. Процедура Minimum(x) возвращает указатель на найденный элемент поддерева с корнем x.

procedure Minimum(x);

begin While left [x] nil do x:= left[x]; Return (x) end;

Алгоритм Maximum симметричен:

procedure Maximum(x);

begin while (right [x] nil) do x:= right[x]; return (x) end;

Оба алгоритма требуют времени O(h), где h – высота дерева (посколь-

289

ку двигаются по дереву только вниз).

Следующий и предыдущий элементы. Если x указатель на неко-

торый узел дерева, то процедура Successor(x) возвращает указатель на узел со следующим за x элементом или nil, если указанный элемент последний в дереве.

procedure Successor(x); begin

If (right[x] nil) then Return Minimum (right[x]); y:= p[x];

while (y nil) and (x=right [y]) do {x:= y; y:= parent[y]}; Return y

end;

Приведенная процедура отдельно рассматривает два случая. Если правое поддерево вершины x не пусто, то следующий за x элемент – минимальный элемент в этом поддереве и он равен Minimum(right[x]). Если правое поддерево вершины x пусто, то идем от x вверх, пока не найдем вершину, являющуюся левым сыном своего родителя. Этот родитель (если он есть) и будет искомым элементом. Время работы процедуры Successor на дереве высоты h есть Ο(h), так как мы двигаемся либо только вверх, либо только вниз. Процедура Predecessor симметрична.

Упражнения

1. Пусть поиск ключа в двоичном дереве завершается в листе. Рассмотрим три множества: A – элементы слева от пути поиска, B – элементы на пути и C – справа от пути. Верно ли, что для любых трех ключей a A, b B и c C выполняются неравенства a b c?

2.Докажите, что k последовательных вызовов процедуры Successor выполняются за Ο(k + h) шагов (h – высота дерева) независимо от того, с какой вершины мы начинаем.

3.Пусть T – двоичное дерево поиска, все ключи в котором различны,

x– его лист, а y – родитель узла x. Покажите, что key [y] является соседним с key [x] ключом (следующим или предыдущим).

Добавление элемента. Процедура Insert (T, z) добавляет заданный элемент в подходящее место дерева T. Параметром процедуры является

указатель z на новую вершину, в которую помещены значения key [z], left[z] = nil и right [z] = nil. В ходе работы процедура изменяет дерево T и (возможно) некоторые поля вершины z, после чего новая вершина с дан-

290