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

advertisement
Двоичные деревья поиска
Определение двоичного
дерева поиска
• Двоичным деревом поиска (ДДП) называют дерево, все вершины
которого упорядочены, каждая вершина имеет не более двух
потомков (назовём их левым и правым), и все вершины, кроме
корня, имеют родителя. Вершины, не имеющие потомков,
называются листами.
• Подразумевается, что каждой вершине соответствует элемент или
несколько элементов, имеющие некие ключевые значения, в
дальнейшем именуемые просто ключами.
• ДДП позволяет выполнять следующие основные операции:
▫ Поиск вершины по ключу.
▫ Определение вершин с минимальным и максимальным значением
ключа.
▫ Переход к предыдущей или последующей вершине, в порядке,
определяемом ключами.
▫ Вставка вершины.
▫ Удаление вершины.
Глубина дерева поиска
• Двоичное дерево может быть логически разбито на уровни.
Корень дерева является нулевым уровнем, потомки корня –
первым уровнем, их потомки – вторым, и т.д.
Глубина дерева это его максимальный уровень.
• Каждую вершину дерева можно рассматривать как корень
поддерева, которое определяется данной вершиной и всеми
потомками этой вершины, как прямыми, так и косвенными.
Поэтому о дереве можно говорить как о рекурсивной структуре.
Сбалансированное
двоичное дерево
• Эффективность поиска по дереву напрямую связана с его
сбалансированностью, то есть с максимальной разницей между
глубиной левого и правого поддерева среди всех вершин. Имеется
два крайних случая – сбалансированное бинарное дерево (где
каждый уровень имеет полный набор вершин) и вырожденное
дерево, где на каждый уровень приходится по одной вершине.
Вырожденное дерево эквивалентно связанному списку.
• Время выполнения всех основных операций пропорционально
глубине дерева. Таким образом, скоростные характеристики
поиска в ДДП могут варьироваться от O(log2N) в случае
законченного дерева до O(N) – в случае вырожденного.
Свойство упорядоченности
• Если x – это произвольная вершина в ДДП, а вершина y находится
в левом поддереве вершины x, то y.key <= x.key.
• Если x – это произвольная вершина ДДП, а вершина y находится в
правом поддереве вершины x, то y.key >= x.key.
• Из свойства следует, что если y.key == x.key, то вершина y может
находиться как в левом, так и в правом поддереве относительно
вершины x. Необходимо помнить, что при наличии нескольких
вершин с одинаковыми значениями ключа некоторые алгоритмы
не будут работать правильно. Например, алгоритм поиска будет
всегда возвращать указатель только на одну вершину. Эту
проблему можно решить, храня элементы с одинаковыми ключами
в одной и той же вершине в виде списка.
Пример ДДП
ДДП
не ДДП
Способы обхода ДДП
• Прямой обход: сначала обходится данная вершина, левое
поддерево данной вершины, затем правое поддерево данной
вершины.
• Поперечный обход: сначала обходится левое поддерево данной
вершины, затем данная вершина, затем правое поддерево данной
вершины. Вершины при этом будут следовать в неубывающем (по
ключам key) порядке.
• Обратный обход: сначала обходится левое поддерево данной
вершины, затем правое, затем данная вершина.
Добавление нового узла
void BinTree::insert(const int &x){
Node *nowNode=root, *parentNode=NIL;
char LorR=0;
while(nowNode!=NIL){ //Пока не найдено подходящее место
parentNode=nowNode;
if (nowNode->data < x)
nowNode=nowNode->right; LorR=+1;
else
nowNode=nowNode->left; LorR=-1;
}
nowNode=new Node(x); // Создание нового узла
nowNode->parent=parentNode;
if (parentNode==NIL){
root=nowNode;
}else{
if(LorR==1)
parentNode->right=nowNode;
else
parentNode->left=nowNode;
}
}
Поиск узлов в поддереве
BinTree::Node* BinTree::_minimum(BinTree::Node *now){
while(now->left!=NULL)
now=now->left;
return now;
}
BinTree::Node* BinTree::_maximum(BinTree::Node *now){
while(now->right!=NULL)
now=now->right;
return now;
}
BinTree::Node* BinTree::_find(const int & x){
Node* now=root;
while(now!=NULL){
if(now->data==x) return now;
if (now->data<x) now=now->right;
else now=now->left;
}
return now;
}
Следующий и предыдущий
BinTree::Node* BinTree::_next(Node *now){
if(now->right!=NIL)
return _minimum(now->right);
Node *nodeParent=now->parent;
while(nodeParent!=NULL && now==nodeParent->right){
now=nodeParent;
nodeParent=nodeParent->parent;
}
return nodeParent;
}
//---------------------------------------------------------BinTree::Node* BinTree::_previous(Node *now){
if(now->left!=NIL)
return _maximum(now->left);
Node *nodeParent=now->parent;
while(nodeParent!=NIL && now==nodeParent->left){
now=nodeParent;
nodeParent=nodeParent->parent;
}
return nodeParent;
}
Балансировка дерева
Алгоритм балансировки:
• Дерево преобразуется в лозу.
• Лоза перестраивается в сбалансированное дерево.
Замечание. Лоза (vine) — это левоассоциативное двоичное дерево.
Преобразование дерева в лозу
Проходим по дереву указателем p, начиная в корне дерева.
Вспомогательный указатель q указывает на родителя p (поскольку
строим левоассоциативное дерево, то p всегда является левым
ребенком q). На каждом шаге возникает одна из двух возможных
ситуаций:
• Если у p нет правого ребенка, то эта часть дерева уже
перестроена. p и q просто спускаются по дереву (приравниваются
своим левым сыновьям).
• Если у p есть правый ребенок (r), тогда выполняется левый
поворот относительно p.
Преобразование лозы в
сбалансированное двоичное дерево
Пусть есть лоза, которая состоит из (2n-1) вершин для какого-либо
натурального n. Для примера возьмем n=4, тогда лоза будет
содержать
15
вершин.
Преобразуем
данную
лозу
в
сбалансированное дерево за три операции перестроения.
На первой операции пройдем по лозе сверху вниз, начиная в корне,
и раскрасим каждую вершину соответственно в красный или
черный цвет (пусть корень будет красного цвета). Затем возьмем
каждую красную вершину, кроме самой нижней, сделаем ее правым
ребенком черной вершины, являющейся ее левым ребенком.
То есть выполним малый правый поворот
относительно каждой правой вершины,
кроме самой нижней (на рисунке приведен
пример
малого
правого
поворота
относительно вершины X).
Пример (продолжение)
Таким образом, вместо лозы, состоящей из 15 вершин, мы получим
дерево, состоящее из 7 черных вершин и 8 серых вершин.
Пример (продолжение)
Для второго перестроения сначала перекрасим красные вершины в
белые. Далее перекрасим каждую вторую черную вершину в
красный цвет, начиная в корне. Теперь, как и раньше, выполним
малый правый поворот относительно каждой красной вершины,
кроме самой нижней.
Пример (продолжение)
Третье перестроение аналогично первым двум. Вершины 12 и 4
перекрашиваются в серый цвет, затем выполняется малый правый
поворот относительно вершины 12. В результате получается
сбалансированное дерево.
Пример (продолжение)
В случае, когда длина лозы не может быть представлена в виде 2n-1
для какого-либо натурального n, необходимо привести длину
главной лозы к требуемому значению.
Пусть лоза состоит из m вершин. Тогда существует такое n, что
(2n-1)<m<(2n+1-1). Необходимо укоротить главную лозу на m-(2n-1)
вершин. После этого можно перестроить получившееся дерево
аналогично способу, описанному выше. В результате получится
сбалансированное дерево с m-(2n-1) листьями.
Для примера разберем случай, когда лоза состоит из 9 вершин.
Отсюда следует, что n=3, т. к. (23-1)=7<9<15=(24-1). Следовательно,
необходимо укоротить главную лозу на 9-(23-1)=2. После этого
перестраиваем дерево аналогично примеру, приведенному выше. В
результате у нас должно получиться сбалансированное дерево.
AA-дерево
Arne Andersson
Для упрощения балансировки дерева вводится понятие уровень
(level) вершины (уровень любой листовой вершины равен 1).
Правило, которому должно удовлетворять AA-дерево:
к одной вершине можно присоединить другую вершину того же
уровня но только одну и только справа.
После добавления узла поднимаемся вверх по дереву и выполняем
балансировку (операции skew и split для каждой вершины).
AA-дерево :: skew
Устранение левой связи на одном уровне
Замечание: горизонтальная стрелка обозначает связь между
вершинами одного уровня, а наклонная (вертикальная) — между
вершинами разного уровня.
AA-дерево :: split
Устранение двух правых связей на одном уровне
Замечание: горизонтальная стрелка обозначает связь между
вершинами одного уровня, а наклонная (вертикальная) — между
вершинами разного уровня.
AA-дерево :: erase
После удаления узла необходимо подняться вверх начиная с
родителя фактически удаленного узла и выполнить балансировку.
Для этого необходимо проверить уровень вершины и если он на 2
больше чем у потомков, то снизить уровень на 1. Если после этого
уровень правого потомка больше уровня в узле, то сделать уровень
правого потомка равным уровню текущего узла. Так как изменение
уровней могло вызвать нарушение правила построения дерева,
необходимо осуществить операцию skew для текущего узла, потом
для правого потомка, потом для правого потомка правого потомка
текущего узла и операцию split для текущего узла и его правого
потомка.
АВЛ-дерево
Г. М. Адельсон-Вельский, Е. М. Ландис (1962)
АВЛ-дерево — сбалансированное по высоте двоичное дерево поиска,
для каждой его вершины высота её двух поддеревьев различается не
более чем на 1.
В каждой вершине хранится степень разбалансированности (-1,0,1)
Балансировкой вершины называется операция, которая в случае
разницы высот левого и правого поддеревьев равной 2, изменяет
связи предок-потомок в поддереве данной вершины так, что
разница становится <= 1, иначе ничего не меняет. Указанный
результат получается вращениями поддерева данной вершины.
АВЛ-дерево: LeftRotate
АВЛ-дерево: RightRotate
АВЛ-дерево: DoubleLeftRotate
АВЛ-дерево: DoubleRightRotate
КРАСНО--ЧЕРНЫЕ
ДЕРЕВЬЯ
(Red-Black Tree, RB-Tree)
кчд
• Одним из способов решения основной проблемы использования
ДДП являются красно-чёрные деревья.
• Красно-чёрные деревья (КЧД) – это ДДП, каждая вершина
которых хранит ещё одно дополнительное логическое поле
(color), обозначающее цвет: красный или чёрный. Фактически, в
КЧД гарантируется, что уровни любых двух листьев отличаются
не более, чем в два раза. Этого условия оказывается достаточно,
чтобы обеспечить скоростные характеристики поиска, близкие к
O(log2N).
• При вставке/замене производятся дополнительные действия по
балансировке дерева, которые не могут не замедлить работу с
деревом.
• При описании алгоритмов мы будем считать, что NIL – это указатель на
фиктивную вершину, и операции (NIL).left, (NIL).right, (NIL).color имеют
смысл. Мы также будем полагать, что каждая вершина имеет двух
потомков, и лишь NIL не имеет потомков.
Свойства КЧД
1.
Каждая вершина может быть либо красной, либо чёрной.
Бесцветных вершин, или вершин другого цвета быть не может.
2. Каждый лист (NIL) имеет чёрный цвет.
3. Если вершина красная, то оба её потомка – чёрные.
4. Все пути от корня к листьям содержат одинаковое число чёрных
вершин.
5. Корень имеет чёрный цвет.
Пример: class RBTree
class RBTree{
private:
struct Node{
int data;
char color;
Node *left, *right, *parent;
Node(const int& x):
data(x),color('b'){left=right=parent=&NilNode;}
Node(Node *L, Node *R, Node *P):
left(L),right(R),parent(P),color('b'){}
static Node NilNode;
};
Node *root;
Node *NIL;
int count;
public:
RBTree(){NIL=&(Node::NilNode); root=NIL;count=0;}
};
//-------------------------------------------------------RBTree::Node RBTree::Node::NilNode(&NilNode,&NilNode,&NilNode);
Вращения
Вращения
void RBTree::_LeftRotate(Node *now){
Node * tmpNode;
tmpNode=now->right;
now->right=tmpNode->left;
if(tmpNode->left!=NIL){
tmpNode->left->parent=now;
}
tmpNode->parent=now->parent;
if(now->parent==NIL)
root=tmpNode;
else{
if (now==now->parent->left)
now->parent->left=tmpNode;
else
now->parent->right=tmpNode;
}
tmpNode->left=now;
now->parent=tmpNode;
}
Добавление
Чтобы добавить вершину в КЧД, после вставки узла (как в
обыкновенное ДДП) необходимо покрасить вершину в красный цвет,
а затем восстановить свойства КЧД. Для этого нужно перекрасить
некоторые вершины и произвести вращения.
После добавления нового узла и покраски его в красный цвет
выполняются все свойства КЧД, кроме, возможно, одного: у новой
красной вершины может быть красный родитель. Такая ситуация
(красная вершина имеет красного родителя) может сохраниться
после любого перекрашивания вершины. Поэтому, до тех пор, пока
рассматриваемый узел и его родитель имеют красный цвет
возможны один из следующих вариантов расположения узлов и
соответствующих им методов перекраски.
Во всех вариантах «дедушка» (родитель родителя добавленного
узла) имеет чёрный цвет, так как пара добавляемый узел и его
родитель были единственным нарушением свойств КЧД.
Добавление. Случай 1
В первом случае «дядя» добавляемого узла – красный узел.
Является ли новая вершина правым или левым потомком своего
родителя, значения не имеет.
Обе вершины (Now и Uncle) – красные, а вершина
Now->Parent->Parent – чёрная. Перекрасим «родителя» и «дядю» в
чёрный цвет, а «дедушку» – в красный. При этом число чёрных
вершин на любом пути от корня к листьям остаётся прежним.
Нарушение свойств КЧД возможно лишь в одном месте: вершина
«дедушка» может иметь красного родителя, поэтому надо
продолжить выполнение проверки, присвоив Now значение
Now->Parent->Parent .
Добавление. Случай 1
«Дядя» – красный узел, правый потомок своего родителя.
Если, добавляемый узел – левый потомок, то принцип раскраски и
её результат не меняются.
Добавление. Случай 1
«Дядя» – красный узел, левый потомок своего родителя.
Если, добавляемый узел – левый потомок, то принцип раскраски и
её результат не меняются.
Добавление. Случай 2
В этом случае «дядя» – чёрная вершина. Добавленный узел (красная
вершина) является левым потомком красной вершины, которая, в
свою очередь, является левым потомком своего родителя, правым
потомком которой является «дядя». В этом случае достаточно
произвести правое вращение и перекрасить две вершины. Процесс
перекраски окончится, так как вершина родитель будет после этого
чёрной.
Добавление. Случай 2
Симметричная ситуация: «дядя» – чёрная вершина, левый потомок
своего родителя. В этом случае достаточно произвести левое
вращение и перекрасить две вершины.
Добавление. Случай 3
В этом случае «дядя» – чёрная вершина. Добавленный узел (красная
вершина) является правым потомком красной вершины, которая, в
свою очередь, является левым потомком своего родителя, правым
потомком которой является «дядя». В этом случае производится
левое вращение, которое сводит этот случай к случаю 2, когда
добавляемый узел является левым потомком своего родителя. После
вращения на путях от корня к листьям остается прежним.
Добавление. Случай 3
Симметричная ситуация: «дядя» – чёрная вершина, левый потомок
своего родителя.
Удаление.
Удаление вершины в КЧД осуществляется также как в
обыкновенном ДДП, но после удаления необходимо осуществить
процедуру восстановления свойств КЧД. Стоит заметить, что
«удаляемая» вершина – это не обязательно та, что содержит
удаляемое значение (ключ), чаще это вершина из которой берётся
значение для замещения.
Очевидно, что если удалили красную вершину, то, поскольку оба ее
потомка чёрные, красная вершина не станет родителем красного
потомка. Если же удалили чёрную вершину, то как минимум на
одном из путей от корня к листьям количество чёрных вершин
уменьшилось. К тому же красная вершина могла стать потомком
красного родителя. Если чёрная вершина была удалена, её черноту
так просто выкидывать нельзя. Она на счету. Поэтому временно
черноту удалённой вершины передали другой вершине и
необходимо её распределить. Она или будет передана красной
вершине (и та станет чёрной) или после перестановок других чёрных
вершин (дабы изменить их количество на пути от корня к листьям)
будет просто выкинута, если дошли до корня.
Удаление. Случай 1
Случай 1 имеет место, когда вершина «брат» красная (в этом случае
родитель – чёрная). Так как оба потомка «брата» чёрные мы можем
поменять цвета «брата» и родителя и произвести левое вращение
вокруг родителя не нарушая свойств КЧД. Вершина остается дважды
чёрной, а её новый брат – чёрным, так что мы свели дело к одному
из следующих случаев.
Удаление. Случай 2
Вершина «брат» – чёрная, и оба её потомка тоже чёрные. В этом
случае можно снять лишнюю чёрноту с вершины (теперь она
единожды чёрная), перекрасить «брата», сделав красной (оба её
потомка чёрные, так что это допустимо) и добавить черноту
родителю. Заметим, что если попали в случай 2 из случая 1, то
родитель – красная вершина. Сделав её чёрной (добавление чёрного
к красной вершине делает её чёрной), мы завершим цикл.
Зелёным и синим, помечены вершины, цвет которых не играет роли,
то есть может быть как черным, так и красным, но сохраняется в
процессе преобразований.
Удаление. Случай 3
Вершина «брат» чёрная, её левый потомок красный, а правый
чёрный. Можно поменять цвета «брата» и её левого потомка и потом
применить правое вращение так, что свойства КЧД будут сохранены.
Новым братом узла будет теперь чёрная вершина с красным правым
потомком, и случай 3 сводится к случаю четыре.
now
brother
Удаление. Случай 4
Вершина «брат» чёрная, правый потомок красный. Меняя
некоторые цвета (не все из них нам известны, но это нам не мешает)
и, производя левое вращение вокруг родителя, мы можем удалить
лишнюю черноту у узла, не нарушая свойств КЧД. На этом можно
закончить преобразования.
Удаление.
Случаи, когда «брат» является левым потомком, симметричны
рассмотренным случаям.
Download