CTree:: treeNil (& treeNil, & treeNil,
& treeNil); p>
Тепер скрізь у класі CTree можна використовувати
змінну treeNil. Переваги очевидні. Витративши якихось дванадцять (3 *
sizeof (CTree *)) байт пам'яті, ми спростили розробку і прискорили виконання
програми. p>
Основна проблема використання ДДП
h2>
Основною проблемою використання ДДП є те, що
методи вставки та видалення вершин, гарантуючи збереження властивості
впорядкованості, абсолютно не сприяють оптимізації основних операцій над
ДДП. Наприклад, якщо вставити в ДДП послідовність зростаючих або
відбувають чисел, воно перетвориться, по суті, в двусвязний список, а основні операції
будуть займати час, пропорційний кількості вершин, а не його логарифму. p>
Таким чином, для отримання продуктивності
порядку O (log2N) потрібно, щоб дерево мало як можна більш високу
збалансованість (тобто мало можливо меншу висоту). Зазвичай виділяються
кілька типів збалансованості. Повна збалансованість, це коли для
кожної вершини дерева кількості вершин у лівому та правому піддерев
розрізняються не більше ніж на 1. На жаль, такої збалансованості важко
досягти на практиці. Тому на практиці використовуються менш жорсткі види
збалансованості. Наприклад, російськими математиками Г. М. Адельсон-Бєльським і
Е. М. Ландіс були розроблені принципи АВЛ дерев. У АВЛ деревах для кожної
вершини дерева глибини обох п?? ддеревьев розрізняються не більше ніж на 1. Ще
одним "просунутим" видом дерев є так звані червоно-чорні
дерева. АВЛ дерева забезпечують більш високу збалансованість дерева, але
витрати на їх підтримку вище. Оскільки на практиці різниця в збалансованості
між цими двома видами дерев не висока, найчастіше використовуються червоно-чорні
дерева. p>
Красно b> - b> чорні b> b> дерева b> (Red-Black Tree , RB-Tree)
b> p>
Отже, одним зі способів вирішення основної проблеми
використання ДДП є червоно-чорні дерева. Червоно-чорні (назва
історично пов'язано з гральними картами, оскільки з них легко робити прості
моделі) дерева (КЧД) - це ДДП, кожна вершина яких зберігає ще одне
додаткове логічне поле (color), що позначає колір: червоний або чорний.
Фактично, в КЧД гарантується, що рівні будь-яких двох листів відрізняються не
більше, ніж в два рази. Цього умови було достатньо, щоб забезпечити
швидкісні характеристики пошуку, близькі до O (log2N). При вставці/заміни
проводяться додаткові дії з балансування дерева, які не можуть
не баритися роботу з деревом. При описі алгоритмів ми будемо вважати, що
NIL - це покажчик на фіктивну вершину, і операції (NIL). Left, (NIL). Right,
(NIL). Color мають сенс. Ми також будемо вважати, що кожна вершина має двох
нащадків, і лише NIL не має нащадків. Таким чином, кожна вершина
стає внутрішньою (що має нащадків, нехай і вигаданих), а листям будуть
лише фіктивні вершини NIL. p>
Властивості КЧД
h2>
Кожна вершина може бути або червоною, або чорною.
Безбарвних вершин, або вершин іншого кольору бути не може. p>
Кожен лист (NIL) має чорний колір. p>
Якщо вершина червона, то обидва її нащадка - чорні. p>
Всі шляхи від кореня до листів містять однакове число
чорних вершин. p>
Приклад КЧД з урахуванням наших положень наведено на
малюнку 4. Врахуйте, що вершина 9 могла бути й червона, але надалі ми будемо
розглядати тільки ті дерева, у яких корінь чорний. Ми це зробимо для
того, щоб нащадки кореня могли мати будь-який колір. p>
p>
Малюнок 4. p>
Обертання
p>
Операції вставки та видалення вершин у КЧД можуть
порушувати властивості КЧД. Щоб відновити ці властивості, треба буде
перефарбовувати деякі вершини і міняти структуру дерева. Для зміни
структури використовуються операції, називані обертанням. Повертаючи КЧД його
властивості, обертання так само відновлюють збалансованість дерева. Обертання
бувають ліві і праві, їх суть показана на малюнку 5. p>
p>
Малюнок 5. p>
Як видно, обертання, переміщаючи вершини, не порушують
властивості впорядкованості. p>
У процедурі RBTLeftRotate передбачається, що
node.right! = NIL. У процедурі RBTRightRotate передбачається, що node.left! =
NIL. P>
RBTLeftRotate (Tree, node) p>
Begin p>
nodeTemp = node.right; p>
node.right = nodeTemp.left; p>
If (nodeTemp.left! = NIL) Then
p>
nodeTemp.left.nodeParent =
node; p>
nodeTemp.nodeParent =
node.nodeParent; p>
If (node.nodeParent == NIL)
Then p>
Tree.root = nodeTemp; p>
Else p>
Begin p>
If (node ==
node.nodeParent.left) Then p>
node.nodeParent.left =
nodeTemp; p>
Else p>
node.nodeParent.right =
nodeTemp; p>
End p>
nodeTemp.left = node; p>
node.nodeParent = nodeTemp; p>
End p>
RBTRightRotate (Tree, node) p>
Begin p>
nodeTemp = node.left; p>
node.left = nodeTemp.right; p>
If (nodeTemp.right! = NIL)
Then p>
nodeTemp.right.nodeParent =
node; p>
nodeTemp.nodeParent =
node.nodeParent; p>
If (node.nodeParent == NIL)
Then p>
Tree.root = nodeTemp; p>
Else p>
Begin p>
If (node ==
node.nodeParent.right) Then p>
node.nodeParent.right =
nodeTemp; p>
Else p>
node.nodeParent.left =
nodeTemp; p>
End p>
nodeTemp.right = node; p>
node.nodeParent = nodeTemp; p>
End p>
Додавання вершини в КЧД
p>
Щоб додати вершину в КЧД, ми застосовуємо процедуру
TreeInsert для ДДП, фарбуємо вершину в червоний колір, а потім відновлюємо
властивості КЧД. Для цього ми перефарбовувати деякі вершини і виробляємо
обертання. p>
1 RBTInsert (Tree, node) p>
2 Begin p>
3 TreeInsert (Tree, node); p>
4 node.color = RED; p>
5 While (node! = Tree.root) and
(node.nodeParent.color == RED) Do p>
6 Begin p>
7 If (node.nodeParent == node.nodeParent.nodeParent.left)
Then p>
8 Begin p>
9 nodeTemp =
node.nodeParent.nodeParent.right; p>
10 If (nodeTemp.color ==
RED) Then p>
11 Begin p>
12 node.nodeParent.color
= BLACK; p>
13 nodeTemp.color =
BLACK; p>
14
node.nodeParent.nodeParent.color = RED; p>
15 node =
node.nodeParent.nodeParent; p>
16 End p>
17 Else p>
18 Begin p>
19 If (node ==
node.nodeParent.right) Then p>
20 Begin p>
21 node =
node.nodeParent; p>
22 RBTLeftRorate (Tree, node); p>
23 End p>
24 node.nodeParent.color
= BLACK; p>
25
node.nodeParent.nodeParent.color = RED; p>
26
RBTRightRotate (Tree, node.nodeParent.nodeParent); p>
27 End p>
28 End p>
29 Else p>
30 Begin p>
31 nodeTemp =
node.nodeParent.nodeParent.left; p>
32 If (nodeTemp.color ==
RED) Then p>
33 Begin p>
34 node.nodeParent.color
= BLACK; p>
35 nodeTemp.color =
BLACK; p>
36
node.nodeParent.nodeParent.color = RED; p>
37 node =
node.nodeParent.nodeParent; p>
38 End p>
39 Else p>
40 Begin p>
41 If (node ==
node.nodeParent.left) Then p>
42 Begin p>
43 node =
node.nodeParent; p>
44
RBTRightRorate (Tree, node); p>
45 End p>
46 node.nodeParent.color = BLACK; p>
47
node.nodeParent.nodeParent.color = RED; p>
48
RBTLeftRotate (Tree, node.nodeParent.nodeParent); p>
49 End p>
50 End p>
51 End p>
52 Tree.root.color = BLACK; p>
53 End p>
Функція RBTInsert не так складне, як здається на перший
погляд. Розглянемо її докладніше. Після строк 3-4 виконуються всі властивості КЧД,
крім, можливо, одного: у нової червоної вершини може бути червоний батько.
Така ситуація (червона вершина має червоного батька) може зберегтися
після будь-якого числа ітерацій циклу. Всередині циклу розглядаються 6 різних
випадків, але три з них (рядки 8-28) симетричні трьом іншим (рядки 30-50),
розходження лише в тому, чи є батько вершини node правим або лівим
нащадком свого батька (випадки розділяються в рядку 7). Тому ми розглянемо
докладно тільки перші три випадки (рядки 8-28). Припустимо, що у всіх
розглянутих КЧД корінь чорний, і будемо підтримувати це властивість (рядок
52). Тому в рядку 5 node.nodeParent (червоного кольору) не може бути коренем,
і node.nodeParent.nodeParent! = NIL. Операції всередині циклу починаються з
знаходження nodeTemp, "дядька" node, то є вершини, що має того ж батька,
що і node.nodeParent. Якщо nodeTemp - червона вершина, то має місце випадок
1, якщо чорна, то 2 або 3. У всіх випадках вершина node.nodeParent.nodeParent
- Чорна, так як пара node, node.nodeParent була єдиним порушенням
властивостей КЧД. p>
Випадок 1 (рядки 12-15 та 34-37) показаний на малюнку 6.
Чи є вершина node правим або лівим нащадком свого батька, значення не
має. p>
p>
Малюнок 6. p>
Обидві вершини (node і nodeTemp) - червоні, а вершина
node.nodeParent.nodeParent - чорна. Перефарбуємо node.nodeParent і nodeTemp в
чорний колір, а node.nodeParent.nodeParent - в червоний. При цьому число чорних
вершин на будь-якому шляху від кореня до листів залишається тим самим. Порушення властивостей КЧД
можливо лише в одному місці: вершина node.nodeParent.nodeParent може мати
червоного батька, тому треба продовжити виконання циклу, присвоївши node
значення node.nodeParent.nodeParent. p>
У випадках 2 і 3 вершина nodeTemp - чорна. Розрізняються
випадки, коли вершина node є правим або лівим нащадком свого батька.
Якщо правим, то це випадок 2 (рядки 20-23 та 41-45). У цьому випадку здійснюється
ліве обертання, яке зводить випадок 2 до випадку 3, коли node є лівим
нащадком свого батька. Так як node і node.nodeParent - червоні, після
обертання кількість чорних вершин на шляхах від кореня до листів залишається тим самим. p>
p>
Малюнок 7. p>
Залишилося розглянути випадок 3: червона вершина node є
лівим нащадком червоної вершини node.nodeParent, яка, в свою чергу,
є лівим нащадком node.nodeParent.nodeParent, правим нащадком якої
є nodeTemp. У цьому випадку досить провести праве обертання і
перефарбувати дві вершини. Цикл скінчиться, так як вершина node.nodeParent буде
після цього чорною. p>
Видалення вершини з КЧД
h2>
Видалення вершини трохи складніше додавання. Ми будемо
вважати, що (NIL). color == BLACK, і будемо вважати операцію взяття кольору у
покажчика, рівного NIL, допустимої операцією. Також ми будемо вважати допустимим
присвоювання (NIL). nodeParent, і будемо вважати дане присвоювання має
результат. Тобто при взятті значення (NIL). NodeParent ми отримаємо раніше
записане значення. Функція RBTDelete подібна TreeDelete, але, видаливши вершину,
вона викликає процедуру RTBDeleteFixUp для відновлення властивостей КЧД. p>
RBTDelete (Tree, node) p>
Begin p>
If (node.left == NIL) or
(node.right == NIL) Then p>
nodeParent = node; p>
Else p>
nodeParent = TreeNext (node); p>
If (nodeParent.left! = NIL)
Then p>
nodeTemp = nodeParent.left; p>
Else p>
nodeTemp = nodeParent.right; p>
nodeTemp.nodeParent =
nodeParent.nodeParent; p>
If (nodeTemp.nodeParent ==
NIL) Then p>
Tree.root = nodeTemp; p>
Else p>
Begin p>
If
(nodeParent.nodeParent.left == nodeParent) Then p>
nodeParent.nodeParent.left
= NodeTemp; p>
Else p>
nodeParent.nodeParent.right = nodeTemp; p>
End p>
If (nodeParent! = node) Then p>
Begin p>
node.key = nodeParent.key; p>
node.color = nodeParent.color; p>
(копіювання додаткових даних) p>
End p>
If (nodeParent.color == BLACK)
Then p>
RBTDeleteFixUp (Tree, nodeTemp); p>
Return nodeParent; p>
End p>
Розглянемо, як процедура RBTDeleteFixUp
відновлює властивості КЧД. Очевидно, що якщо видалили червону вершину, то,
оскільки обидва її нащадка чорні, червона вершина не стане батьком червоного
нащадка. Якщо ж видалили чорну вершину, то як мінімум на одному зі шляхів від
кореня до листів кількість чорних вершин зменшилася. До того ж червона вершина
могла стати нащадком червоного з батьків. p>
1 RTBDeleteFixUp (Tree, node) p>
2 Begin p>
3 While (node! = Tree.root) and (node.color
== BLACK) Do p>
4 Begin p>
5 If (node == node.nodeParent.left) p>
6 Begin p>
7 nodeTemp = node.nodeParent.right; p>
8 If (nodeTemp.color == RED) Then p>
9 Begin p>
10 nodeTemp.color =
BLACK; p>
11
nodeTemp.nodeParent.color = RED; p>
12
RBTLeftRotate (Tree, node.nodeParent); p>
13 nodeTemp =
node.nodeParent.right; p>
14 End p>
15 If (nodeTemp.left.color
== BLACK) and (nodeTemp.right.color == BLACK) Then p>
16 Begin p>
17 nodeTemp.color = RED; p>
18 nodeTemp =
nodeTemp.nodeParent; p>
19 End p>
20 Else p>
21 Begin p>
22 If
(nodeTemp.right.color == BLACK) Then p>
23 Begin p>
24 nodeTemp.left.color
= BLACK; p>
25 nodeTemp.color =
RED; p>
26
RBTRightRotate (Tree, nodeTemp) p>
27 nodeTemp =
node.nodeParent.right; p>
28 End p>
29 nodeTemp.color =
node.nodeParent.color; p>
30 node.color.nodeParent
= BLACK; p>
31 nodeTemp.right.color
= BLACK; p>
32
RBTLeftRotate (Tree, node.nodeParent); p>
33 node = Tree.root; p>
34 End p>
35 End p>
36 Else p>
37 Begin p>
38 nodeTemp =
node.nodeParent.left; p>
39 If (nodeTemp.color == RED) Then p>
40 Begin p>
41 nodeTemp.color =
BLACK; p>
42
nodeTemp.nodeParent.color = RED; p>
43
RBTRightRotate (Tree, node.nodeParent); p>
44 nodeTemp =
node.nodeParent.left; p>
45 End p>
46 If (nodeTemp.right.color
== BLACK) and (nodeTemp.left.color == BLACK) Then p>
47 Begin p>
48 nodeTemp.color = RED; p>
49 nodeTemp =
nodeTemp.nodeParent; p>
50 End p>
51 Else p>
52 Begin p>
53 If
(nodeTemp.left.color == BLACK) Then p>
54 Begin p>
55
nodeTemp.right.color = BLACK; p>
56 nodeTemp.color =
RED; p>
57
RBTLeftRotate (Tree, nodeTemp) p>
58 nodeTemp =
node.nodeParent.left; p>
59 End p>
60 nodeTemp.color =
node.nodeParent.color; p>
61 node.color.nodeParent
= BLACK; p>
62 nodeTemp.left.color =
BLACK; p>
63
RBTRightRotate (Tree, node.nodeParent); p>
64 node = Tree.root; p>
65 End p>
66 End p>
67 End p>
68 node.color = BLACK; p>
69 End p>
Процедура RBTDeleteFixUp застосовується до дерева, яке
має властивості КЧД, якщо врахувати додаткову одиницю чорноти у вершині
node (вона тепер двічі чорна, це чисто логічне поняття, і воно ніде
фактично не зберігається і логічного типу для зберігання кольору вам завжди буде
достатньо) і перетворює його в даний КЧД. p>
Що таке двічі чорна вершина? Це визначення може
заплутати. Формально вершина називається двічі чорною, щоб відбити той факт,
що при підрахунку чорних вершин на шляху від кореня до листа ця вершина вважається
за два чорних. Якщо чорна вершина була вилучена, її чорноту так просто
викидати не можна. Вона на рахунку. Тому тимчасово чорноту віддаленої вершини
передали вершині node. У завдання процедури RBTDeleteFixUp входить розподіл
цієї зайвої чорноти. Вони або буде передана червоною вершині (і та стане
чорною) або після перестановок інших чорних вершин (щоб змінити їх
кількість на шляху від кореня до листів) буде просто викинути. p>
У циклі (рядки 3-67) дерево змінюється, і значення
змінної node теж змінюється, але сформульоване властивість залишається вірним.
Цикл завершується, якщо: p>
node вказує на червону вершину, тоді ми фарбуємо її
в чорний колір (рядок 68). p>
node вказує на корінь дерева, тоді зайва чорнота
може бути просто відсутній. p>
могло виявитися, що всередині тіла циклу вдається
виконати кілька обертань і перефарбувати кілька вершин, так що двічі
чорна вершина зникає. У цьому випадку присвоювання node = Tree.root (рядки 33
і 64) дозволяє вийти з циклу. p>
Всередині циклу node вказує на двічі чорну вершину,
а nodeTemp - на її брата (іншу вершину з тим же батьком). Оскільки вершина
node двічі чорна, nodeTemp не може бути NIL, оскільки в цьому випадку уздовж
одного шляху від node.nodeParent було б більше чорних вершин, ніж уздовж іншого.
Чотири можливих випадку показані на малюнку нижче. Зеленим і синім, помічені
вершини, колір яких не грає ролі, тобто може бути як чорним, так і
червоним, але зберігається в процесі перетворень. p>
p>
Малюнок 8 p>
Переконайтеся, що перетворення не порушують властивість 4
КЧД (пам'ятайте, що вершина node вважається за дві чорні, і що в піддереві a --
f з самого початку не рівну кількість чорних вершин). p>
Випадок 1 (рядки 9-14 та 40-45) має місце, коли
вершина nodeTemp червона (в цьому випадку node.nodeParent чорна). Так як обидва
нащадка вершини nodeTemp чорні ми можемо поміняти кольори nodeTemp і
node.nodeParent і провести ліве обертання навколо node.nodeParent не порушуючи
властивостей КЧД. Вершина node залишається двічі чорною, а її новий брат - чорним, так
що ми звели справу до одного з випадків 2-4. p>
Випадок 2 (рядки 16-19 та 47-50). Вершина nodeTemp --
чорна, і обидва її нащадка теж чорні. У цьому випадку ми можемо зняти зайву
чорноту з node (тепер вона одного разу чорна), перефарбувати nodeTemp, зробивши її
червоною (обидва її нащадка чорні, так що це припустимо) і додати чорноту
батькові node. Зауважимо, що якщо ми потрапили у випадок 2 з випадку 1, то вершина
node.nodeParent - червона. Зробивши її чорної (додавання чорного до червоної
вершині робить її чорною), ми завершимо цикл. p>
Випадок 3 (рядки 23 - 28 і 53 - 59) Вершина nodeTemp
чорна, її лівий нащадок червоний, а правий чорний. Ми можемо поміняти кольори
nodeTemp і її лівого нащадка і потім застосувати праве обертання так, що
властивості КЧД будуть збережені. Новим братом node буде тепер чорна вершина з
червоним правим нащадком, і випадок 3 зводиться до випадку чотири. p>
Випадок 4 (рядки 29 - 33 і 60 - 64) Вершина nodeTemp
чорна, правий нащадок червоний. Змінюючи деякі кольору (не всі з них нам
відомі, але це нам не заважає) і, виробляючи ліве обертання навколо
node.nodeParent, ми можемо видалити зайву чорноту у node, не порушуючи властивостей
КЧД. присвоювання node = Tree.root виводить нас з циклу. p>
Порівняльні характеристики швидкості роботи різних
структур даних
p>
Щоб усе сказане до цього не здавалося порожній
базіканням, я як укладення наведу порівняльні характеристики
швидкості роботи різних структур даних (дерев і масивів). Для вимірювання
часу була використана функція WinAPI QueryPerfomanceCounter. Час
перераховано в мікросекунди. У дужках наведено кінцева глибина дерева.
Тестування проводилося на процесорі Intel Celeron Tualatin 1000MHz з 384Мб оперативної
пам'яті. Як ключ використовувалося число типу int (32-х бітне ціле зі
знаком), а в якості даних число типу float (4-х байтного речовий). p>
Кількість елементів p>
ДДП p>
КЧД p>
Постійно сортованого
масив p>
несортоване масив p>
Масив з постсортіровкой p>
1000 p>
4943 p>
(1000) p>
535 p>
(17) p>
193 p>
32 p>
73 p>
2000 p>
20571
p>
(2000) p>
1127 p>
(19) p>
402 p>
89 p>
152 p>
3000 p>
65819 p>
(3000) p>
1856 p>
(20) p>
621 p>
98 p>
305 p>
4000 p>
82321
p>
(4000) p>
2601 p>
(21) p>
862 p>
189 p>
327 p>
5000 p>
126941
p>
(5000) p>
3328 p>
(22) p>
1153 p>
192 p>
461 p>
6000 p>
183554
p>
(6000) p>
4184 p>
(22) p>
1391 p>
363 p>
652 p>
7000 p>
255561
p>
(7000) p>
4880 p>
(23) p>
1641 p>
452 p>
789 p>
8000 p>
501360
p>
(8000) p>
5479 p>
(23) p>
1905 p>
567 p>
874 p>
9000 p>
1113230
p>
(9000) p>
6253 p>
(24) p>
2154 p>
590 p>
1059 p>
10000 p>
1871090
p>
(10000) p>
7174 p>
(24) p>
2419 p>
662 p>
1180 p>
Таблиця 1. Додавання елементу (зростаючі ключі) p>
Кількість елементів p>
ДДП p>
КЧД p>
Постійно p>
сортованого масив p>
несортоване масив p>
Масив з постсортіровкой p>
1000 p>
4243 p>
108 p>
136 p>
1354 p>
134 p>
2000 p>
19295 p>
250 p>
289 p>
5401 p>
286 p>
3000 p>
71271 p>
391 p>
448 p>
25373 p>
441 p>
4000 p>
79819 p>
560 p>
615 p>
23861 p>
607 p>
5000 p>
124468 p>
759 p>
787 p>
38862 p>
776 p>
6000 p>
180029 p>
956 p>
954 p>
55303 p>
941 p>
7000 p>
246745 p>
1199 p>
1165 p>
75570 p>
1111 p>
8000 p>
487187 p>
1412 p>
1307 p>
98729 p>
1330 p>
9000 p>
1062128 p>
1906 p>
1494 p>
134470 p>
1474 p>
10000 p>
1807235 p>
2068 p>
1719 p>
154774 p>
1688 p>
Таблиця 2. Пошук елементу (зростаючі ключі). p>
Кількість елементів p>
ДДП p>
КЧД p>
Постійно сортованого
масив p>
несортоване масив p>
Масив з постсортіровкой p>
1000 p>
308 p>
419 p>
2077 p>
2047 p>
2080 p>
2000 p>
639 p>
876 p>
13422 p>
8046 p>
8179 p>
3000 p>
1001 p>
1372 p>
25092 p>
30902 p>
18407 p>
4000 p>
1380 p>
1831 p>
32572 p>
32473 p>
32651 p>
5000 p>
1680 p>
2286 p>
50846 p>
51001 p>
50962 p>
6000 p>
2105 p>
2891 p>
73321 p>
73114 p>
73202 p>
7000 p>
2569 p>
3514 p>
99578 p>
100059 p>
99801 p>
8000 p>
3025 p>
4384 p>
129833 p>
129897 p>
130054 p>
9000 p>
3484 p>
5033 p>