Упорядоченные деревья поиска
Обычные деревья не дают выигрыша при хранении множества значений. При поиске элемента все равно необходимо просмотреть все дерево. Однако можно организовать хранение элементов в дереве так, чтобы при поиске элемента достаточно было просмотреть лишь часть дерева. Для этого надо ввести следующее требование упорядоченности дерева:
Двоичное дерево упорядочено, если для любой вершины x справедливо такое свойство: все элементы, хранимые в левом поддереве, меньше элемента, хранимого в x, а все элементы, хранимые в правом поддереве, больше элемента, хранимого в x.
Важное свойство упорядоченного дерева: все элементы его различны. Если в дереве встречаются одинаковые элементы, то такое дерево является частично упорядоченным.
В дальнейшем будет идти речь только о двоичных упорядоченных деревьях, опуская слово «упорядоченный».
Итак, основными операциями, производимыми с упорядоченным деревом, являются:
-
поиск вершины;
-
добавление вершины;
-
удаление вершины;
-
очистка дерева.
Реализацию этих операций приведем в виде соответствующих процедур.
Алгоритм поиска можно записать в рекурсивном виде. Если искомое значение Item меньше Tree^.Data, то поиск продолжается в левом поддереве, если равен – поиск считается успешным, если больше – поиск продолжается в правом поддереве; поиск считается неудачным, если достигли пустого поддерева, а элемент найден не был.
function Find_Tree(Item: TypeElement; Tree: PTree): boolean;
{Поиск вершины дерева, содержащую значение Item}
var
Current: PTree;
begin
Find_Tree := False;
if Tree <> nil then begin {Дерево не пустое}
Current := Tree;
if Current^.Data = Item then {Вершина найдена}
Find_Tree := True
else
if Current^.Data > Item then {Ищем в левом поддереве}
Find_Tree := Find_Tree(Item, Current^.Left)
else {Ищем в правом поддереве}
Find_Tree := Find_Tree(Item, Current^.Right);
end;
end;
Можно написать аналогичную нерекурсивную функцию. Она позволит избежать избыточного хранения информации. Каждый рекурсивный вызов размещает в стеке локальные переменные Item и Tree, а также адрес точки возврата из подпрограммы. В нерекурсивном варианте можно обойтись одной переменной Item и одной переменной Tree.
Как осуществлять нерекурсивный поиск в упорядоченном дереве, рассмотрим на примере алгоритма добавления элемента в дерево. Сначала надо найти вершину, к которой в качестве потомка необходимо добавить новую вершину (фактически произвести поиск), а затем присоединить к найденной новую вершину, содержащую значение Item (процедура написана в предположении, что добавляемого элемента в дереве нет).
procedure Add_Tree(Item: TypeElement; var Tree: PTree);
{Добавление в дерево вершины со значением Item}
var
NewNode, Current: PTree;
begin
if Tree = nil then begin {Дерево пустое}
{Создаем корень}
New(Tree);
Tree^.Data := Item;
Tree^.Left := nil;
Tree^.Right := nil;
end else begin
Current := Tree;
{Поиск вершины}
while ((Item > Current^.Data) and (Current^.Right <> nil)) or
((Item < Current^.Data) and (Current^.Left <> nil)) do
if Item > Current^.Data then
Current := Current^.Right
else
Current := Current^.Left;
{Создание новой вершины}
New(NewNode);
NewNode^.Data := Item;
NewNode^.Left := nil;
NewNode^.Right := nil;
If Item > Current^.Data then {Новая вершина больше найденной}
Current^.Right := NewNode {Присоединяем новую справа}
else {Новая вершина меньше найденной}
Current^.Left := NewNode; {Присоединяем новую слева}
end;
end;
Процедура удаления элемента будет несколько сложнее. При удалении может случиться, что удаляемый элемент находится не в листе, то есть вершина имеет ссылки на реально существующие поддеревья. Эти поддеревья терять нельзя, а присоединить два поддерева на одно освободившееся после удаления место невозможно. Поэтому необходимо поместить на освободившееся место либо самый правый элемент из левого поддерева, либо самый левый из правого поддерева. Нетрудно убедиться, что упорядоченность дерева при этом не нарушится. Договоримся, что будем заменять самый левый элемент из правого поддерева.
Нельзя забывать, что при замене вершина, на которую производится замена, может иметь правое поддерево. Это поддерево необходимо поставить вместо перемещаемой вершины.
procedure Del_Tree(Item: TypeElement; var Tree: PTree);
{Удаление из дерева вершины со значением Item}
var
Parent, Cur, Cur2: PTree;
Direction: (L, R); {направление движения (влево/вправо)}
begin
{Поиск удаляемого элемента}
Parent := nil; {предок удаляемой вершины}
Cur := Tree;
while ((Item > Cur^.Data) and (Cur^.Right <> nil)) or
((Item < Cur^.Data) and (Cur^.Left <> nil)) do begin
Parent := Cur;
if Item > Cur^.Data then begin
Cur := Cur^.Right; Direction := R;
end else begin
Cur := Cur^.Left; Direction := L;
end;
end;
if Cur <> nil then begin {Вершина найдена}
{Анализируем наличие потомков у найденной вершины}
{и удаляем соответствующим образом}
if (Cur^.Left <> nil) and (Cur^.Right <> nil) then begin
{Удаление вершины в случае наличия у нее обоих потомков}
Parent := Cur; Cur2 := Cur^.Right;
{Поиск кандидатуры на замену}
while Cur2^.Left <> nil do begin
Parent := Cur2; Cur2 := Cur2^.Left;
end;
{Заменяем}
Cur^.Data := Cur2^.Data;
{Спасаем правое поддерево вершины, которой заменяем}
if Parent <> Cur then
Parent^.Left := Cur2^.Right
else
Parent^.Right := Cur2^.Right;
Dispose(Cur2);
end else begin
{Удаление вершины в случае наличия у нее}
{не более одного потомка}
if (Cur^.Left = nil) then begin
if Parent <> nil then begin
if Direction = L then
Parent^.Left := Cur^.Right
else
Parent^.Right := Cur^.Right;
end else begin
Tree := Cur^.Right;
end;
end;
if (Cur^.Right = nil) then begin
if Parent <> nil then begin
if Direction = L then
Parent^.Left := Cur^.Left
else
Parent^.Right := Cur^.Left;
end else begin
Tree := Cur^.Left;
end;
end;
Dispose(Cur);
end;
end;
end;
Временная сложность этих алгоритмов (она одинакова для этих алгоритмов, так как в их основе лежит поиск) оценим для наилучшего и наихудшего случая. В лучшем случае, – то есть случае полного двоичного дерева –получаем сложность Omin(log n). В худшем случае дерево может выродиться в список. Такое может произойти, например, при добавлении элементов в порядке возрастания. При работе со списком в среднем придется просмотреть половину списка. Это даст сложность Omax(n).
В процедуре очистки дерева применяется обратный метод обхода дерева. Использование обратного метода обхода гарантирует, что будут сначала посещены и удалены все потомки предка, прежде чем удалится сам предок.
procedure Clear_Tree(var Tree: PTree);
{Очистка дерева}
begin
if Tree <> nil then begin
Clear_Tree(Tree^.Left);
Clear_Tree(Tree^.Right);
Dispose(Tree);
Tree := nil;
end;
end;
Временная сложность этой операции O(n).
- Содержание
- Основные сведения
- Понятия алгоритма и структуры данных
- Анализ сложности и эффективности алгоритмов и структур данных
- Структуры данных
- Элементарные данные
- Данные числовых типов
- Данные целочисленного типа
- Данные вещественного типа
- Операции над данными числовых типов
- Данные символьного типа
- Данные логического типа
- Данные типа указатель
- Линейные структуры данных
- Множество
- Линейные списки
- Линейный однонаправленный список
- Линейный двунаправленный список
- Циклические списки
- Циклический однонаправленный список
- Циклический двунаправленный список
- Разреженные матрицы
- Матрицы с математическим описанием местоположения элементов
- Матрицы со случайным расположением элементов
- Очередь
- Нелинейные структуры данных
- Мультисписки
- Слоеные списки
- Спецификация
- Реализация
- Деревья
- Общие сведения
- Обходы деревьев
- Спецификация двоичных деревьев
- Реализация
- Основные операции
- Организация
- Представление файлов b-деревьями
- Основные операции
- Общая оценка b-деревьев
- Алгоритмы обработки данных
- Методы разработки алгоритмов
- Метод декомпозиции
- Динамическое программирование
- Поиск с возвратом
- Метод ветвей и границ
- Метод альфа-бета отсечения
- Локальные и глобальные оптимальные решения
- Алгоритмы поиска
- Поиск в линейных структурах
- Последовательный (линейный) поиск
- Бинарный поиск
- Хеширование данных
- Функция хеширования
- Открытое хеширование
- Закрытое хеширование
- Реструктуризация хеш-таблиц
- Поиск по вторичным ключам
- Инвертированные индексы
- Битовые карты
- Использование деревьев в задачах поиска
- Упорядоченные деревья поиска
- Случайные деревья поиска
- Оптимальные деревья поиска
- Сбалансированные по высоте деревья поиска
- Поиск в тексте
- Прямой поиск
- Алгоритм Кнута, Мориса и Пратта
- Алгоритм Боуера и Мура
- Алгоритмы кодирования (сжатия) данных
- Общие сведения
- Метод Хаффмана. Оптимальные префиксные коды
- Кодовые деревья
- Алгоритмы сортировки
- Основные сведения. Внутренняя и внешняя сортировка
- Алгоритмы внутренней сортировки
- Сортировка подсчетом
- Сортировка простым включением
- Сортировка методом Шелла
- Сортировка простым извлечением.
- Древесная сортировка
- Сортировка методом пузырька
- Быстрая сортировка (Хоара)
- Сортировка слиянием
- Сортировка распределением
- Сравнение алгоритмов внутренней сортировки
- Алгоритмы внешней сортировки
- Алгоритмы на графах
- Алгоритм определения циклов
- Алгоритмы обхода графа
- Поиск в глубину
- Поиск в ширину (Волновой алгоритм)
- Нахождение кратчайшего пути
- Алгоритм Дейкстры
- Алгоритм Флойда
- Переборные алгоритмы
- Нахождение минимального остовного дерева
- Алгоритм Прима
- Алгоритм Крускала
- 190000, Санкт-Петербург, ул. Б. Морская, 67