Учебная работа. Реферат: Двоичные деревья поиска

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (Пока оценок нет)
Загрузка...
Контрольные рефераты

Учебная работа. Реферат: Двоичные деревья поиска

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

Роман Акопов

Определение Двоичного Дерева Поиска (Binary Search Tree, BST)

Двоичным деревом поиска (ДДП) именуют дерево, все верхушки которого упорядочены, любая верхушка имеет не наиболее 2-ух потомков (назовём их левым и правым), и все верхушки, не считая корня, имеют родителя. Верхушки, не имеющие потомков, именуются листами. Предполагается, что каждой верхушке соответствует элемент либо несколько частей, имеющие некоторые главные значения, в предстоящем называемые просто ключами. Обычно одной верхушке соответствует один элемент, потому данные определения можно без утраты смысла считать синонимами, хотя и нужно держать в голове, что в неких реализациях это не так. В приведённых методах считается, что одной верхушке соответствует лишь один элемент. Потому мы будем употреблять понятия ключа верхушки и данных верхушки, подразумевая ключ и данные соответственного верхушке элемента. Мы так же будем осознавать под вставкой верхушки добавление верхушки с обозначенным значением элемента и присвоение указателям на родителя и потомков корректных значений. Конкретно ключ употребляется во всех операциях сопоставления частей. Элемент может также содержать ассоциированные с ключом данные. На практике в качестве ключа может употребляться часть данных элемента. Ключ также может храниться как отдельное

Поиск верхушки по ключу.

Определение вершин с наименьшим и наибольшим значением ключа.

Переход к предшествующей либо следующей верхушке, в порядке, определяемом ключами.

Вставка верхушки.

Удаление верхушки.

Двоичное дерево быть может логически разбито на уровни. Корень дерева является нулевым уровнем, потомки корня – первым уровнем, их потомки – вторым, и т.д. Глубина дерева это его наибольший уровень. понятие глубины также быть может описано в определениях пути, другими словами глубина дерева есть длина самого длинноватого пути от корня до листа, если следовать от родительской верхушки до потомка. Каждую верхушку дерева можно разглядывать как корень поддерева, которое определяется данной верхушкой и всеми потомками данной верхушки, как прямыми, так и косвенными. Потому о дереве можно гласить как о рекурсивной структуре. Эффективность поиска по дереву впрямую связана с его сбалансированностью, другими словами с наибольшей различием меж глубиной левого и правого поддерева посреди всех вершин. Имеется два последних варианта – равновесное бинарное дерево (где любой уровень имеет полный набор вершин) и вырожденное дерево, где на любой уровень приходится по одной верхушке. Вырожденное дерево эквивалентно связанному списку. время выполнения всех главных операций пропорционально глубине дерева. Таковым образом, скоростные свойства поиска в ДДП могут варьироваться от O(log2N) в случае законченного дерева до O(N) – в случае вырожденного.

ДДП быть может применено для реализации таковых абстракций, как сортированный перечень, словарь (набор соответствий «ключ-

При реализации дерева кроме значения ключа (key) и данных также хранятся три указателя: на родителя (net), левого (left) и правого (right) потомков. Если родителя либо потомка нет, то указатель хранит нулевое (NULL, NIL)

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

Если x – это случайная верхушка в ДДП, а верхушка y находится в левом поддереве верхушки x, то y.key <= x.key. Если x – это случайная верхушка ДДП, а верхушка y находится в правом поддереве верхушки x, то y.key >= x.key. Из характеристики следует, что если y.key == x.key, то верхушка y может находиться как в левом, так и в правом поддереве относительно верхушки x.

нужно держать в голове, что при наличии нескольких вершин с схожими значениями ключа некие методы не будут работать верно. К примеру, метод поиска будет постоянно возвращать указатель лишь на одну верхушку. Эту делему можно решить, храня элементы с схожими ключами в одной и той же верхушке в виде перечня. В таком случае мы будем хранить в одной верхушке несколько частей, но данный вариант в статье не рассматривается.

Это двоичное дерево поиска:

Набросок 1.

А это нет:

Набросок 2.

методы обхода ДДП

Есть три метода обхода: Прямой (preorder), Поперечный (inorder), Оборотный (postorder).

Прямой обход: поначалу обходится данная верхушка, левое поддерево данной верхушки, потом правое поддерево данной верхушки.

Поперечный обход: поначалу обходится левое поддерево данной верхушки, потом данная верхушка, потом правое поддерево данной верхушки. Верхушки при всем этом будут следовать в неубывающем (по ключам key) порядке.

Оборотный обход: поначалу обходится левое поддерево данной верхушки, потом правое, потом данная верхушка.

На рисунке 3 порядок обхода вершин указан номерами, при всем этом предполагается, что сами верхушки размещены так, что образуют ДДП.

Набросок 3.

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

Поиск верхушки в ДДП

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

Рекурсивно:

TreeSearch(node, key)

Begin

// Если верхушка равна NIL, то нечего в ней находить. Так же и возвращаем.

// Это необходимо для поиска по не имеющимся потомкам

If (node == NIL) Then

Return node;

// Если отыскали, то возвращаем указатель на найденную верхушку.

If (node.key == key) Then

Return node;

// Если ключ отысканной верхушки больше того, который мы отыскиваем

If (node.key > key) Then

// То находить в левом поддереве

Return TreeSearch(node.left, key);

Else

// По другому в правом поддереве

Return TreeSearch(node.right, key);

End

ПРИМЕЧАНИЕ

В прилагаемом начальном коде, написанном на паскале-подобном языке, все характеристики передаются по значению. nodeParent, nodeTemp и node – это указатели на верхушки, а Tree – само дерево, имеющее поле root, указатель на корень дерева.

Итеративно:

TreeSearch(node,key)

Begin

// Пока ещё есть верхушки посреди которых можно находить

//(мы просматриваем не все, но несколько) и пока мы не отыскали

While (node != NIL) and (node.key != key) Do

Begin

// Если ключ найденногй верхушки больше того который мы отыскиваем

If (node.key > key) Then

node = node.left; // То находить в левом поддереве

Else

node = node.right; // А по другому в правом поддереве

End

Return node; // Вернуть отысканное

End

Поиск верхушки с наименьшим и наибольшим значением ключа

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

TreeMinimum(node)

Begin

While (node.left != NIL) Do // Пока есть левый потомок

Node = node.left; // Перейти к нему

Return node;

End

TreeMaximum(node)

Begin

While (node.right != NIL) Do // Пока есть правый потомок

node = node.right; // Перейти к нему

Return node;

End

Нахождение последующей и предшествующей верхушки в ДДП

Чтоб отыскать предшествующую и последующую верхушку, нужно опять вспомянуть свойство упорядоченности. Разглядим это на примере функции TreeNext. Она учитывает два варианта. Если правое поддерево не пусто, то верхушка из правого поддерева с наименьшим значением ключа и будет последующей. Если же правое поддерево пусто, тогда мы идём ввысь, пока не найдём верхушку, являющуюся левым потомком собственного родителя. Этот родитель (если он есть) и будет последующей верхушкой. Возвращаемое

TreeNext(node)

Begin

// Если правое поддерево не пусто, то вернуть

// верхушку с наименьшим значением ключа из правого поддерева

If (node.right != NIL) Then

Return TreeMinimum(node.right);

nodeParent = node.nodeParent;

// Перебирать родителей, пока не найдена верхушка,

// являющаяся левым потомком собственного родителя

// либо пока не завершатся предки.

While (nodeParent != NIL) and (node == nodeParent.right) Do

Begin

node = nodeParent;

nodeParent = nodeParent.nodeParent;

End

// Вернуть родителя верхушки, являющегося левым потомком собственного родителя

Return nodeParent;

End

TreePrevious(node)

Begin

If (node.left != NIL) Then

// Если левое поддерево не пусто, то вернуть

// верхушку из левого поддерева с наибольшим значением ключа

Return TreeMaximum(node.left);

nodeParent = node.nodeParent;

// Перебирать родителей, пока не найдём верхушку, являющуюся

// правым потомком собственного родителя либо пока не завершатся предки

While (nodeParent != NIL) and (node == nodeParent.left) Do

Begin

node = nodeParent;

nodeParent = nodeParent.nodeParent;

End

// Вернуть родителя верхушки являющегося его правым потомком

Return nodeParent;

End

Добавление верхушки

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

TreeInsert(Tree,node)

Begin

nodeParent = NIL;

nodeTemp = T.root;

// Пока ещё есть верхушки которые нужно просмотреть, то

// есть пока мы не добрались до “листочков” дерева

While (nodeTemp != NIL) Do

Begin

nodeParent = nodeTemp;

// Если ключ верхушки, которую мы желаем вставить,

// меньше ключа текущей верхушки

If (node.key < nodeTemp.key) Then

nodeTemp = nodeTemp.left; // То он должен быть в его левом поддереве

Else

nodeTemp = nodeTemp.right; // А по другому в правом

End

node.nodeParent = nodeParent;

If (nodeParent == NIL) Then // Если в дереве ещё нет вершин

Tree.root = node; // То добавить первую

Else

Begin

// Если ключ верхушки, которую мы желаем вставить,

// меньше ключа верхушки, потомком которой обязана стать

// вставляемая верхушка

If (node.key < nodeParent.key) Then

nodeParent.left = node; // То добавить в дерево как левого потомка

Else

nodeParent.right = node; // По другому добавить в дерево как правого потомка

End

End

Удаление верхушки

Препядствия появляются и при удалении. Нам нужно сохранить свойство упорядоченности ДДП. При удалении вероятны три варианта: у удаляемой верхушки нет потомков, у удаляемой верхушки есть один потомок и у удаляемой верхушки два потомка. Если потомков нет, то верхушку можно просто удалить. Если потомок один, то удаляемую верхушку можно “вырезать”, указав её родителю в качестве потомка единственного имеющегося потомка удаляемой верхушки. Если же потомков два, требуются доп деяния. необходимо отыскать последующую за удаляемой (по порядку ключей) верхушку, скопировать её содержимое (ключ и данные) в удаляемую верхушку (она сейчас никуда не удаляется на физическом уровне, хотя логически исчезает) и удалить найденную верхушку (у неё не будет левого потомка). Поначалу функция TreeDelete отыскивает верхушку, которую нужно удалить, потом переменной nodeTemp присваивается указатель на имеющегося потомка удаляемой верхушки (либо NIL, если потомков нет). Дальше верхушка удаляется из дерева, при всем этом раздельно рассматриваются случаи: когда потомков нет и когда удаляемая верхушка – это корень дерева. Возвращаемое память. Момент её настоящего удаления зависит от применяемых способов распределения памяти.

TreeDelete(Tree,node)

Begin

// Если потомков не наиболее 1-го (случаи 1 и 2)

If (node.left == NIL) or (node.right == NIL) Then

del = node; // на физическом уровне удаляем текущую верхушку

Else

del = TreeNext(node); // По другому последующую

If (del.left != NIL) Then // Пытаемся отыскать хоть 1-го потомка

nodeTemp = del.left;

Else

nodeTemp = del.right;

// Если есть, родителем потомка делаем родителя

// удаляемой верхушки (вариант 2)

If (nodeTemp != NIL) Then

nodeTemp.nodeParent = del.nodeParent;

// Если удаляем корень дерева, нужно указать новейший корень дерева

If (del.nodeParent == NIL) Then

Tree.root = nodeTemp;

Else

Begin

// Указываем родителю удаляемой верхушки качестве потомка

// потомок удаляемой верхушки

If (del.nodeParent.left == del) Then

del.nodeParent.left = nodeTemp;

Else

del.nodeParent.right = nodeTemp;

End

If (del != node) Then // Если вариант 3

Begin

node.key = del.key; // Скопировать ключ

{ копирование доп данных }

End

Return del;

End

NIL, NULL и мелкие хитрости

Часто методы, просто выглядящие на бумаге, стают нагромождением сплошных конструкций if в настоящей программке. Почему? Ответ предельно ясен: почти все методы для работы с деревьями подразумевают, что (NIL).parent == (NIL).left == (NIL).right == NIL. Вроде всё ясно и даже разумно, но ведь в почти всех языках программирования NIL/NULL – это ноль. А воззвание по нулевому адресу памяти чревато плохими вещами. Что все-таки созодать? Ведь не достаточно того, что все эти if тормозят программку, в их просто запутаться! Решение просто: мы не будем употреблять NIL! Вправду, методам совсем всё равно, какое численное адресок переменной, проинициализированной особенным образом. Я покажу это на языке С++, но думаю, этот пример можно будет перевести и на остальные языки, хотя там, быстрее всего, нет шаблонов, и придется пожертвовать типобезопасностью.

template <class CTree>class CTreeBase

{

protected:

CTree * lpCParent;

CTree * lpCLeft;

CTree * lpCRight;

public:

CTreeBase(CTreeBase * lpCParentInit, CTreeBase * lpCLeftInit,

CTreeBase * lpCRightInit)

{

lpCParent = (CTree *)lpCParentInit;

lpCLeft = (CTree *)lpCLeftInit;

lpCRight = (CTree *)lpCRightInit;

}

};

/////////////////////////////////////

class CTree : public CTreeBase<CTree>

{

private:

int data;

protected:

static CTreeBase<CTree> treeNil;

};

////////////////////////////////////////////////////////////

CTreeBase<CTree> CTree::treeNil(&treeNil, &treeNil, &treeNil);

сейчас всюду в классе CTree можно употреблять переменную treeNil. Достоинства явны. Потратив каких-либо двенадцать (3 * sizeof(CTree *)) б памяти, мы упростили разработку и убыстрили выполнение программки.

Основная неувязка использования ДДП

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

Таковым образом, для получения производительности порядка O(log2N) необходимо, чтоб дерево имело как можно наиболее высшую сбалансированность (другими словами имело может быть наименьшую высоту). Обычно выделяются несколько типов сбалансированности. Полная сбалансированность, это когда для каждой верхушки дерева количества вершин в левом и правом поддеревьях различаются не наиболее чем на 1. К огорчению, таковой сбалансированности тяжело достигнуть на практике. Потому на практике употребляются наименее твердые виды сбалансированности. к примеру, русскими математиками Г. М. Адельсон-Вельским и Е.М.Ландисом были разработаны принципы АВЛ деревьев. В АВЛ деревьях для каждой верхушки дерева глубины обоих поддеревьев различаются не наиболее чем на 1. Еще одним “продвинутым” видом деревьев является так именуемые красно-чёрные деревья. АВЛ деревья обеспечивают наиболее высшую сбалансированность дерева, но Издержки на их поддержание выше. Так как на практике разница в сбалансированности меж этими 2-мя видами деревьев не высока, почаще употребляются красно-чёрные деревья.

Красно

чёрные
деревья
(Red-Black Tree, RB-Tree)

Итак, одним из методов решения главный задачи использования ДДП являются красно-чёрные деревья. Красно-чёрные (заглавие исторически соединено с игральными картами, так как из их просто созодать обыкновенные модели) деревья (КЧД) – это ДДП, любая верхушка которых хранит ещё одно доп логическое поле (color), обозначающее цвет: красноватый либо чёрный. Практически, в КЧД гарантируется, что уровни всех 2-ух листьев различаются не наиболее, чем вдвое. Этого условия оказывается довольно, чтоб обеспечить скоростные свойства поиска, близкие к O(log2N). При вставке/подмене выполняются доп деяния по балансировке дерева, которые не могут не замедлить работу с деревом. При описании алгоритмов мы будем считать, что NIL – это указатель на фиктивную верхушку, и операции (NIL).left, (NIL).right, (NIL).color имеют смысл. Мы также будем считать, что любая верхушка имеет 2-ух потомков, и только NIL не имеет потомков. Таковым образом, любая верхушка становится внутренней (имеющей потомков, пусть и фиктивных), а листьями будут только фиктивные верхушки NIL.

характеристики КЧД

Любая верхушка быть может или красноватой, или чёрной. Тусклых вершин, либо вершин другого цвета быть не может.

Любой лист (NIL) имеет чёрный цвет.

Если верхушка красноватая, то оба её потомка – чёрные.

Все пути от корня к листьям содержат однообразное число чёрных вершин.

Пример КЧД с учётом наших положений приведен на рисунке 4. Учтите, что верхушка 9 могла быть и красноватой, но в предстоящем мы будем разглядывать лишь те деревья, у каких корень чёрный. Мы это создадим для того, чтоб потомки корня могли иметь хоть какой цвет.

Набросок 4.

Вращения

Операции вставки и удаления вершин в КЧД могут нарушать характеристики КЧД. Чтоб вернуть эти характеристики, нужно будет перекрашивать некие верхушки и поменять структуру дерева. Для конфигурации структуры употребляются операции, именуемые вращением. Возвращая КЧД его характеристики, вращения так же восстанавливают сбалансированность дерева. Вращения бывают левые и правые, их сущность показана на рисунке 5.

Набросок 5.

Как видно, вращения, перемещая верхушки, не нарушают характеристики упорядоченности.

В процедуре RBTLeftRotate предполагается, что node.right != NIL. В процедуре RBTRightRotate предполагается, что node.left != NIL.

RBTLeftRotate(Tree,node)

Begin

nodeTemp = node.right;

node.right = nodeTemp.left;

If (nodeTemp.left != NIL) Then

nodeTemp.left.nodeParent = node;

nodeTemp.nodeParent = node.nodeParent;

If (node.nodeParent == NIL) Then

Tree.root = nodeTemp;

Else

Begin

If (node == node.nodeParent.left) Then

node.nodeParent.left = nodeTemp;

Else

node.nodeParent.right = nodeTemp;

End

nodeTemp.left = node;

node.nodeParent = nodeTemp;

End

RBTRightRotate(Tree,node)

Begin

nodeTemp = node.left;

node.left = nodeTemp.right;

If (nodeTemp.right != NIL) Then

nodeTemp.right.nodeParent = node;

nodeTemp.nodeParent = node.nodeParent;

If (node.nodeParent == NIL) Then

Tree.root = nodeTemp;

Else

Begin

If (node == node.nodeParent.right) Then

node.nodeParent.right = nodeTemp;

Else

node.nodeParent.left = nodeTemp;

End

nodeTemp.right = node;

node.nodeParent = nodeTemp;

End

Добавление верхушки в КЧД

Чтоб добавить верхушку в КЧД, мы применяем функцию TreeInsert для ДДП, красим верхушку в красноватый цвет, а потом восстанавливаем характеристики КЧД. Для этого мы перекрашиваем некие верхушки и производим вращения.

1 RBTInsert(Tree,node)

2 Begin

3 TreeInsert(Tree,node);

4 node.color = RED;

5 While (node != Tree.root) and (node.nodeParent.color == RED) Do

6 Begin

7 If (node.nodeParent == node.nodeParent.nodeParent.left) Then

8 Begin

9 nodeTemp = node.nodeParent.nodeParent.right;

10 If (nodeTemp.color == RED) Then

11 Begin

12 node.nodeParent.color = BLACK;

13 nodeTemp.color = BLACK;

14 node.nodeParent.nodeParent.color = RED;

15 node = node.nodeParent.nodeParent;

16 End

17 Else

18 Begin

19 If (node == node.nodeParent.right) Then

20 Begin

21 node = node.nodeParent;

22 RBTLeftRorate(Tree,node);

23 End

24 node.nodeParent.color = BLACK;

25 node.nodeParent.nodeParent.color = RED;

26 RBTRightRotate(Tree,node.nodeParent.nodeParent);

27 End

28 End

29 Else

30 Begin

31 nodeTemp = node.nodeParent.nodeParent.left;

32 If (nodeTemp.color == RED) Then

33 Begin

34 node.nodeParent.color = BLACK;

35 nodeTemp.color = BLACK;

36 node.nodeParent.nodeParent.color = RED;

37 node = node.nodeParent.nodeParent;

38 End

39 Else

40 Begin

41 If (node == node.nodeParent.left) Then

42 Begin

43 node = node.nodeParent;

44 RBTRightRorate(Tree,node);

45 End

46 node.nodeParent.color = BLACK;

47 node.nodeParent.nodeParent.color = RED;

48 RBTLeftRotate(Tree,node.nodeParent.nodeParent);

49 End

50 End

51 End

52 Tree.root.color = BLACK;

53 End

Функция RBTInsert не так сложна, как кажется на 1-ый взор. Разглядим её подробнее. Опосля строк 3-4 производятся все характеристики КЧД, не считая, может быть, 1-го: у новейшей красноватой верхушки быть может красноватый родитель . Таковая ситуация (красноватая верхушка имеет красноватого родителя) может сохраниться опосля хоть какого числа итераций цикла. Снутри цикла рассматриваются 6 разных случаев, но три из их (строчки 8-28) симметричны трём иным (строчки 30-50), различие только в том, является ли родитель верхушки node правым либо левым потомком собственного родителя (случаи делятся в строке 7). Потому мы разглядим тщательно лишь 1-ые три варианта (строчки 8-28). Представим, что во всех рассматриваемых КЧД корень чёрный, и будем поддерживать это свойство (строчка 52). Потому в строке 5 node.nodeParent (красноватого цвета) не быть может корнем, и node.nodeParent.nodeParent != NIL. Операции снутри цикла начинаются с нахождения nodeTemp, “дяди” node, другими словами верхушки, имеющей такого же родителя, что и node.nodeParent. Если nodeTemp – красноватая верхушка, то имеет пространство вариант 1, если темная, то 2 либо 3. Во всех вариантах верхушка node.nodeParent.nodeParent – чёрная, потому что пара node, node.nodeParent была единственным нарушением параметров КЧД.

Вариант 1 (строчки 12-15 и 34-37) показан на рисунке 6. Является ли верхушка node правым либо левым потомком собственного родителя, значения не имеет.

Набросок 6.

Обе верхушки (node и nodeTemp) – красноватые, а верхушка node.nodeParent.nodeParent – чёрная. Перекрасим node.nodeParent и nodeTemp в чёрный цвет, а node.nodeParent.nodeParent – в красноватый. При всем этом число чёрных вершин на любом пути от корня к листьям остаётся прежним. Нарушение параметров КЧД может быть только в одном месте: верхушка node.nodeParent.nodeParent может иметь красноватого родителя, потому нужно продолжить выполнение цикла, присвоив node

В вариантах 2 и 3 верхушка nodeTemp – чёрная. Различаются случаи, когда верхушка node является правым либо левым потомком собственного родителя. Если правым, то это вариант 2 (строчки 20-23 и 41-45). В этом случае делается левое вращение, которое сводит вариант 2 к случаю 3, когда node является левым потомком собственного родителя. Потому что node и node.nodeParent – красноватые, опосля вращения количество чёрных вершин на путях от корня к листьям остается прежним.

Набросок 7.

Осталось разглядеть вариант 3: красноватая верхушка node является левым потомком красноватой верхушки node.nodeParent, которая, в свою очередь, является левым потомком node.nodeParent.nodeParent, правым потомком которой является nodeTemp. В этом случае довольно произвести правое вращение и перекрасить две верхушки. Цикл окончится, потому что верхушка node.nodeParent будет опосля этого чёрной.

Удаление верхушки из КЧД

Удаление верхушки мало труднее прибавления. Мы будем считать, что (NIL).color == BLACK, и будем считать операцию взятия цвета у указателя, равного NIL, допустимой операцией. Также мы будем считать допустимым присваивание (NIL).nodeParent, и будем считать данное присваивание имеющим итог. Другими словами при взятии значения (NIL).nodeParent мы получим ранее записанное

RBTDelete(Tree,node)

Begin

If (node.left == NIL) or (node.right == NIL) Then

nodeParent = node;

Else

nodeParent = TreeNext(node);

If (nodeParent.left != NIL) Then

nodeTemp = nodeParent.left;

Else

nodeTemp = nodeParent.right;

nodeTemp.nodeParent = nodeParent.nodeParent;

If (nodeTemp.nodeParent == NIL) Then

Tree.root = nodeTemp;

Else

Begin

If (nodeParent.nodeParent.left == nodeParent) Then

nodeParent.nodeParent.left = nodeTemp;

Else

nodeParent.nodeParent.right = nodeTemp;

End

If (nodeParent != node) Then

Begin

node.key = nodeParent.key;

node.color = nodeParent.color;

{ копирование доп данных }

End

If (nodeParent.color == BLACK) Then

RBTDeleteFixUp(Tree,nodeTemp);

Return nodeParent;

End

Разглядим, как процедура RBTDeleteFixUp восстанавливает характеристики КЧД. Разумеется, что если удалили красноватую верхушку, то, так как оба ее потомка чёрные, красноватая верхушка не станет родителем красноватого потомка. Если же удалили чёрную верхушку, то как минимум на одном из путей от корня к листьям количество чёрных вершин уменьшилось. К тому же красноватая верхушка могла стать потомком красноватого родителя.

1 RTBDeleteFixUp(Tree,node)

2 Begin

3 While (node != Tree.root) and (node.color == BLACK) Do

4 Begin

5 If (node == node.nodeParent.left)

6 Begin

7 nodeTemp = node.nodeParent.right;

8 If (nodeTemp.color == RED) Then

9 Begin

10 nodeTemp.color = BLACK;

11 nodeTemp.nodeParent.color = RED;

12 RBTLeftRotate(Tree,node.nodeParent);

13 nodeTemp = node.nodeParent.right;

14 End

15 If (nodeTemp.left.color == BLACK) and (nodeTemp.right.color == BLACK) Then

16 Begin

17 nodeTemp.color = RED;

18 nodeTemp = nodeTemp.nodeParent;

19 End

20 Else

21 Begin

22 If (nodeTemp.right.color == BLACK) Then

23 Begin

24 nodeTemp.left.color = BLACK;

25 nodeTemp.color = RED;

26 RBTRightRotate(Tree,nodeTemp)

27 nodeTemp = node.nodeParent.right;

28 End

29 nodeTemp.color = node.nodeParent.color;

30 node.color.nodeParent = BLACK;

31 nodeTemp.right.color = BLACK;

32 RBTLeftRotate(Tree,node.nodeParent);

33 node = Tree.root;

34 End

35 End

36 Else

37 Begin

38 nodeTemp = node.nodeParent.left;

39 If (nodeTemp.color == RED) Then

40 Begin

41 nodeTemp.color = BLACK;

42 nodeTemp.nodeParent.color = RED;

43 RBTRightRotate(Tree,node.nodeParent);

44 nodeTemp = node.nodeParent.left;

45 End

46 If (nodeTemp.right.color == BLACK) and (nodeTemp.left.color == BLACK) Then

47 Begin

48 nodeTemp.color = RED;

49 nodeTemp = nodeTemp.nodeParent;

50 End

51 Else

52 Begin

53 If (nodeTemp.left.color == BLACK) Then

54 Begin

55 nodeTemp.right.color = BLACK;

56 nodeTemp.color = RED;

57 RBTLeftRotate(Tree,nodeTemp)

58 nodeTemp = node.nodeParent.left;

59 End

60 nodeTemp.color = node.nodeParent.color;

61 node.color.nodeParent = BLACK;

62 nodeTemp.left.color = BLACK;

63 RBTRightRotate(Tree,node.nodeParent);

64 node = Tree.root;

65 End

66 End

67 End

68 node.color = BLACK;

69 End

Процедура RBTDeleteFixUp применяется к дереву, которое владеет качествами КЧД, если учитывать доп единицу черноты в верхушке node (она сейчас два раза чёрная, это чисто логическое понятие, и оно нигде практически не сохраняется и логического типа для хранения цвета для вас постоянно будет довольно) и превращает его в истинное КЧД.

Что такое два раза чёрная верхушка? Это определение может запутать. Формально верхушка именуется два раза чёрной, чтобы отразить тот факт, что при подсчёте чёрных вершин на пути от корня до листа эта верхушка считается за две темных. Если чёрная верхушка была удалена, её черноту так просто выбрасывать недозволено. Она на счету. Потому временно черноту удалённой верхушки передали верхушке node. В задачку процедуры RBTDeleteFixUp заходит распределение данной излишней черноты. Они либо будет передана красноватой верхушке (и та станет чёрной) либо опосля перестановок остальных чёрных вершин (чтобы поменять их количество на пути от корня к листьям) будет просто выкинута.

В цикле (строчки 3-67) дерево меняется, и

node показывает на красноватую верхушку, тогда мы красим её в чёрный цвет (строчка 68).

node показывает на корень дерева, тогда лишняя чернота быть может просто удалена.

Могло оказаться, что снутри тела цикла удается выполнить несколько вращений и перекрасить несколько вершин, так что два раза чёрная верхушка исчезает. В этом случае присваивание node = Tree.root (строчки 33 и 64) дозволяет выйти из цикла.

Снутри цикла node показывает на два раза чёрную верхушку, а nodeTemp – на её брата (другую верхушку с этим же родителем). Так как верхушка node два раза чёрная, nodeTemp не быть может NIL, так как в этом случае вдоль 1-го пути от node.nodeParent было бы больше чёрных вершин, чем вдоль другого. Четыре вероятных варианта показаны на рисунке ниже. Зелёным и голубым, помечены верхушки, цвет которых не играет роли, другими словами быть может как черным, так и красноватым, но сохраняется в процессе преобразований.

Набросок 8

Удостоверьтесь, что преобразования не нарушают свойство 4 КЧД (помните, что верхушка node считается за две чёрные, и что в поддеревьях a — f вначале не равное количество чёрных вершин).

Вариант 1 (строчки 9-14 и 40-45) имеет пространство, когда верхушка nodeTemp красноватая (в этом случае node.nodeParent чёрная). Потому что оба потомка верхушки nodeTemp чёрные мы можем поменять цвета nodeTemp и node.nodeParent и произвести левое вращение вокруг node.nodeParent не нарушая параметров КЧД. Верхушка node остается два раза чёрной, а её новейший брат – чёрным, так что мы свели дело к одному из случаев 2-4.

Вариант 2 (строчки 16-19 и 47-50). Верхушка nodeTemp – чёрная, и оба её потомка тоже чёрные. В этом случае мы можем снять лишнюю чёрноту с node (сейчас она единожды чёрная), перекрасить nodeTemp, сделав ёё красноватой (оба её потомка чёрные, так что это допустимо) и добавить черноту родителю node. Заметим, что если мы попали в вариант 2 из варианта 1, то верхушка node.nodeParent – красноватая. Сделав её чёрной (добавление чёрного к красноватой верхушке делает её чёрной), мы завершим цикл.

Вариант 3 (строчки 23 – 28 и 53 — 59) Верхушка nodeTemp чёрная, её левый потомок красноватый, а правый чёрный. Мы можем поменять цвета nodeTemp и её левого потомка и позже применить правое вращение так, что характеристики КЧД будут сохранены. Новеньким братом node будет сейчас чёрная верхушка с красноватым правым потомком, и вариант 3 сводится к случаю четыре.

Вариант 4 (строчки 29 – 33 и 60 — 64) Верхушка nodeTemp чёрная, правый потомок красноватый. Меняя некие цвета (не все из их нам известны, но это нам не мешает) и, производя левое вращение вокруг node.nodeParent, мы можем удалить лишнюю черноту у node, не нарушая параметров КЧД. присваивание node = Tree.root выводит нас из цикла.

Сравнительные свойства скорости работы разных структур данных

Чтоб всё произнесенное ранее не чудилось пустой болтовнёй, я в качестве заключения приведу сравнительные свойства скорости работы разных структур данных (деревьев и массивов). Для измерения времени была применена функция WinAPI QueryPerfomanceCounter. время пересчитано в микросекунды. В скобках приведена конечная глубина дерева. Тестирование проводилось на микропроцессоре Intel Celeron Tualatin 1000MHz с 384Мб оперативки. В качестве ключа использовалось число типа int (32-х битное целое со знаком), а в качестве данных число типа float (4-х байтное вещественное).

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Несортированный массив

Массив с постсортировкой

1000

4943

(1000)

535

(17)

193

32

73

2000

20571

(2000)

1127

(19)

402

89

152

3000

65819

(3000)

1856

(20)

621

98

305

4000

82321

(4000)

2601

(21)

862

189

327

5000

126941

(5000)

3328

(22)

1153

192

461

6000

183554

(6000)

4184

(22)

1391

363

652

7000

255561

(7000)

4880

(23)

1641

452

789

8000

501360

(8000)

5479

(23)

1905

567

874

9000

1113230

(9000)

6253

(24)

2154

590

1059

10000

1871090

(10000)

7174

(24)

2419

662

1180

Таблица 1. Добавление элемента (растущие ключи)

количество частей

ДДП

КЧД

Повсевременно

сортированный массив

Несортированный массив

Массив с постсортировкой

1000

4243

108

136

1354

134

2000

19295

250

289

5401

286

3000

71271

391

448

25373

441

4000

79819

560

615

23861

607

5000

124468

759

787

38862

776

6000

180029

956

954

55303

941

7000

246745

1199

1165

75570

1111

8000

487187

1412

1307

98729

1330

9000

1062128

1906

1494

134470

1474

10000

1807235

2068

1719

154774

1688

Таблица 2. Поиск элемента (растущие ключи).

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Несортированный массив

Массив с постсортировкой

1000

308

419

2077

2047

2080

2000

639

876

13422

8046

8179

3000

1001

1372

25092

30902

18407

4000

1380

1831

32572

32473

32651

5000

1680

2286

50846

51001

50962

6000

2105

2891

73321

73114

73202

7000

2569

3514

99578

100059

99801

8000

3025

4384

129833

129897

130054

9000

3484

5033

164846

194361

164264

10000

4050

5690

203207

202979

202738

Таблица 3. Удаление элемента по ключу (растущие ключи).

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Несортированный массив

Массив с постсортировкой

1000

547

(25)

548

(13)

1800

34

362

2000

1133

(25)

1171

(14)

5534

70

734

3000

1723

(28)

1905

(14)

12065

150

1174

4000

2891

(28)

2985

(15)

20867

223

1626

5000

3604

(28)

4024

(15)

32927

248

1962

6000

4336

(29)

4970

(15)

44720

373

2537

7000

5196

(29)

5794

(16)

60723

443

2977

8000

6051

(30)

6972

(16)

77482

511

3485

9000

6994

(30)

7519

(16)

104219

590

3821

10000

9544

(31)

10303

(16)

118760

584

4408

Таблица 4. Добавление элемента (случайные ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Несортированный массив

Массив с постсортировкой

1000

181

136

159

1354

155

2000

406

307

347

5412

339

3000

653

494

551

12927

538

4000

925

702

765

23936

747

5000

1223

949

1024

37861

962

6000

1532

1142

1216

55124

1185

7000

1893

1494

1453

75628

1403

8000

2477

1833

1679

98802

1631

9000

2943

2390

1994

125570

1858

10000

3395

2937

2228

154791

2108

Таблица 5. Поиск элемента (случайные ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Несортированный массив

Массив с постсортировкой

1000

469

517

1149

2014

1195

2000

995

1079

4381

8054

4387

3000

1557

1756

9673

18191

9639

4000

2272

2424

17071

32451

17105

5000

3070

3019

27380

50665

26954

6000

3528

3618

39294

72996

39227

7000

4322

4542

53255

99402

53309

8000

5299

5531

71406

129964

70766

9000

6180

6553

89583

164943

89935

10000

7527

7797

112190

202993

111439

Таблица 6. Удаление элемента по ключу (случайные ключи)

Повсевременно сортированный массив – это массив, в который элементы вставляются так, что бы он сохранял свойство упорядоченности. Массив с постсортировкой – это массив, в который поначалу вставляются все элементы, а позже он сортируется методом резвой сортировки. Данные таблицы, непременно, не являются Правду в крайней инстанции, но дозволят для вас прикинуть, какая из структур данных будет более производительна в вашей программке, беря во внимание предполагаемую возможность операций вставки, удаления и поиска. Так, к примеру, массив с постсортировкой является очень симпатичным по производительности, но совсем не подступает для всеохватывающих работ, потому что подразумевает фиксированный порядок действий – поначалу лишь добавление частей, а опосля внедрение приобретенной инфы. Остальные структуры данных лишены этого недочета. Для огромного числа (порядка 10 000) случайных частей время поиска в ДДП и КЧД становится фактически схожим. Как следствие, можно воплотить наиболее обыкновенные методы, исходя из неких параметров входных данных. С иной стороны, в последнем случае (растущие элементы) ДДП отстают от КЧД на несколько порядков. Повсевременно сортированный массив является абсолютным победителем по скорости, но не имеет естественной поддержки отношений родитель-ребёнок. Если они для вас необходимы, придётся реализовывать их поддержку без помощи других. Также нужно постоянно держать в голове, что при количестве частей порядка тыщи, асимптотические характеристики скорости ещё не полностью вступили в силу и на теоретическом уровне наиболее резвые структуры данных на практике могут оказаться наиболее неспешными. Так, к примеру, скорость поиска в КЧД и массиве с пресортировкой до 5000-7000 фактически схожа. Так же нужно увидеть, что тестирование выполнялось на объектах относительно малого размера (8 б), сопоставимого с размером указателя (4 б). Все виды массивов сортированных предполагают очень интенсивное копирование частей, в то время как деревья работают с указателями. При размере элемента, на порядки превосходящем размеры указателя, акценты сместятся очень существенно. к примеру, ниже приведены результаты тесты с ключом типа int (32-x битное целое) и битовыми данными размером в 256 б.

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Не сортированный массив

Массив с постсортировкой

1000

5868

(1000)

1663

(17)

1430

1154

1035

2000

140888

(2000)

3694

(19)

3476

2460

2808

3000

368748

(3000)

5815

(20)

5372

3848

4382

4000

721328

(4000)

7271

(21)

7274

5175

6035

5000

1208373

(5000)

9349

(22)

9247

6670

7619

6000

1752135

(6000)

11395

(22)

11046

7778

9168

7000

2501167

(7000)

13503

(23)

13327

9378

10764

8000

3334948

(8000)

15753

(23)

18222

12560

15267

9000

4266560

(9000)

21600

(24)

20564

15391

17430

10000

5421499

(10000)

24105

(24)

24064

17152

19341

Таблица 7. Добавление элемента (растущие ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Не сортированный массив

Массив с постсортировкой

1000

4289

132

242

1621

230

2000

134074

303

605

6903

530

3000

359985

457

961

24268

806

4000

706129

787

1336

27610

1121

5000

1183405

959

1736

44660

1516

6000

1730699

1116

2138

69068

1841

7000

2462759

1344

2494

103158

2251

8000

3293519

1565

2871

159274

2617

9000

4215750

1840

3284

278697

2923

10000

5361524

2060

3698

513268

3303

Таблица 8. Поиск элемента (растущие ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Не сортированный массив

Массив с постсортировкой

1000

502

583

115640

131837

186703

2000

1181

1152

1604342

1574484

1592896

3000

1602

1580

4616940

4653355

4604626

4000

2075

2537

8966113

8999484

8978279

5000

2689

3007

14848795

14851393

14822206

6000

3574

3836

21572736

21473162

21676597

7000

4129

4432

30384061

29938188

30409709

8000

4898

5424

39013182

39342862

39341616

9000

5086

6368

50197296

49946077

50320092

10000

6279

6372

63020912

62049584

62564125

Таблица 9. Удаление элемента по ключу (растущие ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Не сортированный массив

Массив с постсортировкой

1000

1903

(24)

2072

(12)

57991

1418

5448

2000

4532

(25)

4739

(14)

479463

3107

13907

3000

7747

(26)

7819

(15)

1727433

4992

22440

4000

10348

(29)

10664

(15)

3616654

6733

33905

5000

13064

(29)

13652

(16)

6141684

8314

43768

6000

16530

(31)

16713

(16)

9214638

10191

53619

7000

19305

(31)

19642

(16)

12981887

11904

61301

8000

23140

(32)

23329

(16)

17388765

13785

75968

9000

26051

(32)

26378

(16)

21935279

15335

92007

10000

29450

(32)

29448

(16)

27053495

17075

90155

Таблица 10. Добавление элемента (случайные ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Не сортированный массив

Массив с постсортировкой

1000

185

150

266

1613

274

2000

695

359

719

6974

724

3000

1044

586

1193

15561

1245

4000

2186

823

1694

27675

1703

5000

2701

1106

2234

44905

2314

6000

3898

1496

2874

71549

2871

7000

4883

1889

3464

109554

3371

8000

4186

2183

4060

165605

4077

9000

6760

2771

4696

281860

4622

10000

7291

3201

5372

514893

5384

Таблица 11. Поиск элемента (случайные ключи)

количество частей

ДДП

КЧД

Повсевременно сортированный массив

Не сортированный массив

Массив с постсортировкой

1000

1235

1115

54929

111088

62794

2000

3042

2978

557875

1596298

558580

3000

4641

4703

1837401

4730623

1841029

4000

7531

6494

3830510

9008129

3833983

5000

9497

8788

6675616

14599142

6626964

6000

12018

10922

10270460

21832592

10315160

7000

14605

14376

14808484

29779691

14618091

8000

15876

16070

19927348

39932636

19946118

9000

20043

19079

25347571

49928153

25384886

10000

22117

21860

32049086

61766884

32072537

Таблица 12. Удаление элемента по ключу (случайные ключи)

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

]]>