Красно-черные деревья
Красно-черные деревья - один из способов балансировки деревьев. Название происходит от стандартной раскраски узлов таких деревьев
в красный и черный цвета.
13.1 Свойства красно-черных деревьев
Красно-черное дерево представляет собой бинарное дерево поиска с одним дополнительным битом цвета в каждом узле. Цвет узла может
быть либо красным, либо черным. В соответствии с накладываемыми на узлы дерева ограничениями, ни один путь в красно-черном дереве
не отличается от другого по длине более чем в два раза, так что красно-черные деревья являются приближенно сбалансированными.
Цвета узлов используются при балансировке дерева. Во время операций вставки и удаления поддеревья может
понадобиться повернуть, чтобы достигнуть сбалансированности дерева.
Бинарное дерево поиска является красно-черным деревом, если оно удовлетворяет следующим красно-черным свойствам.
- Каждый узел является красным или черным.
- Корень дерева является черным.
- Каждый лист дерева (nil) является черным.
- Если узел — красный, то оба его дочерних узла — черные.
- Для каждого узла все пути от него до листьев, являющихся потомками данного узла, содержат одно и то же количество черных узлов.
Красно-черные деревья хорошо использовать в качестве деревьев поиска, т.к.
Красно-черное дерево с n внутренними узлами имеет
высоту не более чем 2*log (n + 1).
Оценкой времени операции (как вставки, так и удаления) над КСД с N узлами является O(logn).
Теоретическая информация о Red-Black Tree доступна здесь:
Алголист: Красно-черные деревья
Приведу ООП-программу, реализующую основные операции с КЧД (создание и удаление дерева; поиск, удаление элемента; и вывод
дерева на экран или в файл):
uses crt;
type
string_20 = string[20]; { Это - тип данных, хранимых в КЧД }
{ Узел дерева }
PTreeNode = ^TreeNode;
TreeNode = object
public
constructor init(_s: string; _parent: PTreeNode);
public
Color: integer; { Цвет листа: 0 = Черный, 1 = Красный }
left, right, parent,
duplicates: PTreeNode; { Список дубликатов (не стал удалять, иногда это нужно) }
data_string: string_20;
deleted: boolean; { Флаг для "ленивого" удаления }
end;
RBTree = object
public
constructor init;
destructor done;
function Search(s: string): integer;
{ Возвращает +1, если строка присутствует в дереве, и -1 если ее там нет }
function SearchFirstMatch(s: string): PTreeNode;
{ Работает точно так же, как и Search, но возвращает указатель
типа PTreeNode на первый подходящий элемент }
procedure Insert(s: string);
{ Добавляет новый элемент в дерево }
function InsertItem(s: string; node: PTreeNode): PTreeNode;
function Remove(s: string): boolean;
{ Удаляет заданную строку }
procedure LoadFromFile(fn: string); { Загружает дерево из текстового файла (не реализовано) }
procedure SaveToFile(var f: text); { Сохраняет дерево в текстовый файл }
function LeftDepth: integer; { Находит глубину левого поддерева }
function RightDepth: integer; { Находит глубину правого поддерева }
function NumberOfLeaves(p: PTreeNode): integer;
{ Находит число листьев в дереве }
public
root: PTreeNode;
private
procedure LeftRotation(node: PTreeNode);
procedure RightRotation(node: PTreeNode);
{ "Ротации" (повороты), используемые при вставке для балансировки дерева }
function TreeDepth(p: PTreeNode): integer;
{ Рекурсивная функция для нахождения глубины дерева, с корнем в P }
procedure DeleteTree(p: PTreeNode);
{ Деструктор вызывает эту процедуру для удаления дерева }
procedure SaveNode(level: integer;
const node: PTreeNode; var f: text);
{ Рекурсивная процедура сохранения узла в тестовый файл F }
end;
Const
{ Цвета для узлов }
node_Red = 1;
node_Black = 0;
constructor TreeNode.init(_s: string; _parent: PTreeNode);
begin
data_string := _s;
left := nil; right := nil;
parent := _parent; { Указатель на предка }
Duplicates := nil; { Изначально у узла нет дубликатов }
Color := node_Red; { Новые узлы становятся "Красными" }
Deleted := False; { Этот узел не удален }
end;
{
Функция сравнения строк (ведомые пробелы не принимаются во внимание)
}
function compare(s1, s2: string): integer;
procedure trim(var s: string);
begin
while s[length(s)] = ' ' do delete(s, length(s), 1);
end;
begin
trim(s1); trim(s2);
if s1 < s2 then compare := -1
else if s1 > s2 then compare := +1
else compare := 0;
end;
constructor RBTree.init;
begin
root := nil;
end;
destructor RBTree.done;
begin
DeleteTree(Root);
{ DeleteTree освобождает динамическую память }
end;
procedure RBTree.DeleteTree(p: PTreeNode);
begin
if p <> nil then begin
DeleteTree(p^.Left); { Удалить левое поддерево }
DeleteTree(p^.Right); { Удалить правое поддерево }
DeleteTree(p^.Duplicates);
dispose(p);
end;
p := nil; { Узел более не используется }
end;
{
При вставке элемента могут произойти 3 случая:
1. Новый узел и его "дядя" - "Красные"
2. Новый узел красный, "дядя" - "Черный", и узел - левый потомок
3. Новый узел красный, "дядя" - "Черный", и узел - правый потомок
}
procedure RBTree.Insert(s: string);
{ Создает новый узел для хранения строки }
var
node, node_2: PTreeNode;
begin
node := InsertItem(s, root); { Вставить строку в дерево }
if node = nil then exit; { Изменять дерево не нужно }
while(node <> root) and (node^.parent^.color = node_Red) do begin
{ Проверяем, находится ли узел в левом поддереве }
if node^.parent = node^.parent^.parent^.left then begin
node_2 := node^.parent^.parent^.right; { Делаем node2 "дядей" нашего узла }
{ Если "дядя" красного цвета - это случай 1 }
if (node_2 <> nil) and (node_2^.Color = node_Red) then begin
node^.parent^.Color := node_Black; { Изменяем цвет "родителя" на черный }
node_2^.Color := node_Black; { Изменяем цвет "дяди" на черный }
node^.Parent^.Parent^.Color := node_Red; { Делаем "дедушку" красным }
node := node^.Parent^.Parent; { Двигаемся к вершине дерева для проведения дополнительных исправлений }
end
else begin { "дядя" - черного цвета, случай 2 или 3 }
if Node = Node^.Parent^.Right then begin { Проверяем на случай №3 }
Node := Node^.Parent; { Узел - правый потомок, это как раз случай №3... }
LeftRotation(Node); { ... который требует левую "ротацию" }
end;
Node^.Parent^.Color := node_Black; { Установка для случаев №2... }
Node^.Parent^.Parent^.Color := node_Red; { ... и №3 }
RightRotation(Node^.Parent^.Parent);
end;
end
else begin { узел - в правом поддереве }
node_2 := Node^.Parent^.Parent^.Left; { Делаем node2 "дядей" нашего узла }
{ Если "дядя" красного цвета - это случай 1 }
if(node_2 <> nil) and (node_2^.Color = node_Red) then begin
Node^.Parent^.Color := node_Black; { Изменяем цвет "родителя" на черный }
Node_2^.Color := node_Black; { Изменяем цвет "дяди" на черный }
Node^.Parent^.Parent^.Color := node_Red; { Делаем "дедушку" красным }
Node := Node^.Parent^.Parent; { Двигаемся к вершине дерева... }
end
else begin { "дядя" - "черный", случай №2 или №3 }
{ Проверяем на случай №3 ("лево" и "право" обменяны местами) }
if Node = Node^.Parent^.Left then begin
Node := Node^.Parent; { Узел - левый потомок, это как раз случай №3... }
RightRotation(Node); { ... который требует правую "ротацию" }
end;
Node^.Parent^.Color := node_Black; { Установка для случаев №2... }
Node^.Parent^.Parent^.Color := node_Red; { ... и №3 }
LeftRotation(Node^.Parent^.Parent);
end;
end
end;
{ По правилу КЧД корень дерева должен быть черным }
Root^.Color := node_Black;
end;
function RBTree.InsertItem(s: string; node: PTreeNode): PTreeNode;
var
comparison: integer;
GreaterThanLeft, LessThanRight: boolean;
T: PTreeNode;
begin
if root = nil then begin
root := new(PTreeNode, init(s, nil)); { устанавливаем корень }
{ По правилу КЧД корень дерева должен быть черным }
root^.Color := node_Black;
InsertItem := root; exit
end;
while True do begin
comparison := compare(s, node^.data_string);
if node^.Deleted then begin
{ Для начала проверим, является ли узел "удаленным".
Если это так, то существует возможность использовать "удаленный" узел
для хранения новой записи, если она должна будет находиться
между двумя "потомками" }
if node^.Left = nil then GreaterThanLeft := true
else
{ (В случае, если compare() < 0): строка не больше чем левый потомок,
поэтому "удаленный" узел не может использоваться для хранения новой записи }
GreaterThanLeft := (compare(s, node^.left^.data_string) > 0);
if node^.Right = nil then LessThanRight := true
else
{ (В случае, если compare() < 0): строка не больше чем правый потомок,
поэтому "удаленный" узел не может использоваться для хранения новой записи }
LessThanRight := (compare(s, node^.right^.data_string) > 0);
if GreaterThanLeft and LessThanRight then begin
{ "удаленный" узел может использоваться для хранения новой записи }
node^.data_string := s;
node^.Deleted := false; { удел больше "удаленным" не считать }
InsertItem := nil; exit
{
возвращаем NIL, чтобы избежать "ротаций" дерева, т.к. элемент, значение
которого было изменено, находится на своем месте
}
end;
end;
if comparison < 0 then begin
{ Если Left пусто, помещаем новый узел туда }
if Node^.Left = nil then begin
Node^.Left := new(PTreeNode, init(s, Node)); { Добавляем новый узел ... }
InsertItem := Node^.Left; { ... как левого потомка }
exit
end
else Node := Node^.Left; { Проверить левое поддерево }
end
else
if comparison > 0 then begin
{ Если Right пусто, помещаем новый узел туда }
if Node^.Right = nil then begin
Node^.Right := new(PTreeNode, init(s, Node)); { Добавляем новый узел ... }
InsertItem := Node^.Right; { ... как правого потомка }
exit
end
else Node := Node^.Right; { Проверить правое поддерево }
end
else begin { узел - дубликат }
T := node;
{ находим конец списка дубликатов }
while(T^.Duplicates <> nil) do T := T^.Duplicates;
T^.Duplicates := new(PTreeNode, init(s, T));
InsertItem := nil; exit
{
возвращаем NIL, чтобы избежать "ротаций" дерева, т.к.
мы просто изменили список дубликатов
}
end;
end;
end;
function RBTree.Remove(s: string): boolean;
var T, prev_node, node: PTreeNode;
begin
Remove := False;
Node := SearchFirstMatch(s); { Найдем подходящий узел в дереве }
if node = nil then exit; { Строка не была найдена - выход }
if node^.Duplicates <> nil then begin
{
если есть дубликаты - то один из дубликатов может занять
место удаляемой записи
}
T := node;
while T^.Duplicates <> nil do begin
prev_node := T;
T := T^.Duplicates;
end;
node^.data_string := T^.data_string;
{ Копируем содержимое последнего дубликата в ту запись, которую будем удалять }
dispose(T);
prev_node^.Duplicates := nil;
{ "отсекаем" последний элемент списка дубликатов }
Remove := true; { удаление было успешным }
end
else
Node^.Deleted := true; { Помечаем узел как "удаленный" для "ленивого" удаления }
Remove := True
end;
function RBTree.Search(s: string): integer;
var
node: PTreeNode;
comparison: integer;
begin
Search := -1;
node := root;
while Node <> nil do begin
comparison := compare(s, node^.data_string);
if comparison < 0 then Node := Node^.Left { просматриваем левое поддерево }
else
if comparison < 0 then Node := Node^.Right { просматриваем правое поддерево }
else
if Node^.Deleted then exit
{ если узел помечен на удаление - то не принимать его во внимание, выход }
else begin
{ Строка найдена }
search := 1; exit
end;
end;
{ Запись не найдена }
end;
function RBTree.SearchFirstMatch(s: string): PTreeNode;
{ Возвращает указатель на первый узел, хранящий заданную строку }
var
node: PTreeNode;
comparison: integer;
begin
SearchFirstMatch := nil;
node := root;
while Node <> nil do begin
comparison := compare(s, node^.data_string);
if comparison < 0 then Node := Node^.Left { просматриваем левое поддерево }
else
if comparison > 0 then Node := Node^.Right { просматриваем правое поддерево }
else
if Node^.Deleted then exit
{ если узел помечен на удаление - то не принимать его во внимание, выход }
else begin
{ Строка найдена }
SearchFirstMatch := node; exit
end;
end;
end;
procedure RBTree.SaveToFile(var f: text);
{
сохраняет узлы в Прямом (нисходящем) порядке
}
begin
{ Вызываем рекурсию }
SaveNode(0, root, f);
end;
procedure RBTree.SaveNode(level: integer;
const node: PTreeNode; var f: text);
const
_color: array[0 .. 1] of char = ('B', 'R');
begin
if node <> nil then begin
if not node^.Deleted then begin
writeln(f, '':3*level,
node^.data_string + ' ('+_color[node^.Color]+')');
end;
SaveNode(level + 1, node^.Left, f); { Сохраним узлы левого поддерева }
SaveNode(level + 1, node^.Right, f); { Сохраним узлы правого поддерева }
end;
end;
procedure RBTree.LoadFromFile(fn: string);
begin
(* Не реализовано *)
end;
function RBTree.LeftDepth: integer;
begin
LeftDepth := TreeDepth(Root^.Left); { Измеряем левое поддерево }
end;
function RBTree.RightDepth: integer;
begin
RightDepth := TreeDepth(Root^.Right); { Измеряем правое поддерево }
end;
function RBTree.TreeDepth(p: PTreeNode): integer;
var _left, _right: integer;
begin
_left := 0; _right := 0;
if p^.Left <> nil then
_left := TreeDepth(p^.Left); { Взять глубину левого поддерева }
if p^.Right <> nil then
_right := TreeDepth(p^.Right); { Взять глубину правого поддерева }
if _left > _right then { проверяем, какое поддерево "глубже" }
TreeDepth := _left + 1 { вернем глубину левого поддерева + 1 }
else
TreeDepth := _right + 1; { вернем глубину правого поддерева + 1 }
end;
function RBTree.NumberOfLeaves(p: PTreeNode): integer;
var total: integer;
begin
NumberOfLeaves := 1;
total := 0;
if (p^.Left = nil) and (p^.Right = nil) then exit; { узел является "листом" }
{ считаем число листьев в левом поддереве }
if p^.Left <> nil then inc(total, NumberOfLeaves(p^.Left));
{ считаем число листьев в правом поддереве }
if p^.Right <> nil then inc(total, NumberOfLeaves(p^.Right));
NumberOfLeaves := total;
{ и возвращаем общее количество листьев в дереве }
end;
procedure RBTree.LeftRotation(node: PTreeNode);
var Right: PTreeNode;
begin
Right := node^.Right; { hold node's right child }
{ make the node's right child its right child's left child }
node^.Right := Right^.Left;
if Right^.Left <> nil then
Right^.Left^.Parent := Node; { point the child to its new parent }
if Right <> nil then
Right^.Parent := Node^.Parent; { point the child to its new parent }
if Node^.Parent <> nil then begin { if node is not the root }
if Node = Node^.Parent^.Left then { if node is a left child }
Node^.Parent^.Left := Right { make node's right child its parent's left child }
else
Node^.Parent^.Right := Right; { make node's right child its parent's right child }
end
else
Root := Right; { node's right child is now the root }
Right^.Left := Node; { node becomes its right child's left child }
if Node <> nil then
Node^.Parent := Right; { point node to its new parent }
end;
procedure RBTree.RightRotation(node: PTreeNode);
var Left: PTreeNode;
begin
Left := node^.Left; { hold node's left child }
{ make the node's left child its left child's right child }
Node^.Left := Left^.Right;
if Left^.Right <> nil then
Left^.Right^.Parent := Node; { point the child to its new parent }
if Left <> nil then
Left^.Parent := Node^.Parent; { point the child to its new parent }
if Node^.Parent <> nil then begin { if node is not the root }
if Node = Node^.Parent^.Right then { if node is a right child }
Node^.Parent^.Right := Left { make node's left child its parent's right child }
else
Node^.Parent^.Left := Left; { make node's left child its parent's left child }
end
else
Root := Left; { node's left child is now the root }
Left^.Right := Node; { node becomes its left child's right child }
if Node <> nil then
Node^.Parent := Left; { point node to its new parent }
end;
{ Собственно, программа, иллюстрирующая использование КЧД }
var
console: text;
s: string_20;
tree: RBTree;
begin
assigncrt(console);
rewrite(console);
tree.init;
{
Вводим следующую последовательность строк:
one
two
three
four
five
}
repeat
write('enter new string (20 chars max): '); readln(s);
if s <> '' then begin
tree.insert(s);
Writeln('**');
tree.SaveToFile(console); { Выводим дерево на консоль }
writeln('**');
end;
until s = '';
tree.SaveToFile(console);
{ Проверяем работу Search }
if tree.search('four') = 1 then writeln('found')
else writeln('not found');
{ Проверяем работу Remove }
tree.Remove('four');
tree.SaveToFile(console);
tree.done;
close(console);
end.
