Тема 6 БІНАРНІ ДЕРЕВА ПОШУКУ Будь - який дурень може написати код, що зрозумілий комп'ютерові. Гарні програмісти пишуть код, що зрозумілий людям… (Мартін Фаулер ) Всяке дерево, що не приносить плоду доброго, зрубують й кидають у вогонь. (Єв. від Матфея)
6. 1 Вступ • Дерева пошуку являють собою структури даних, що підтримують багато операцій з динамічними масивами, включаючи пошук елемента, мінімального і максимального значення, вставку і видалення. Таким чином, дерево пошуку може використовуватися і як словник, і як черга з пріоритетами. • Основні операції в бінарному дереві пошуку виконуються за час, пропорційний його висоті. Для повного бінарного дерева з n вузлами ці операції виконуються за час (log 2 n) у найгіршому випадку. Математичне чекання висоти побудованого випадковим образом бінарного дерева дорівнює О (log 2 n), так що всі основні операції над динамічною множиною в такому дереві виконуються в середньому за час у (log 2 n).
6. 1 Вступ • Як випливає з назви, бінарне дерево пошуку в першу чергу є бінарним деревом, як показано на мал. 6. 1. Таке дерево може бути представлене за допомогою зв'язаної структури даних, у якій кожен вузол є об'єктом. На додаток до полів ключа key і супутних даних, кожен вузол містить поля left, right і р, що вказують на ліві і правий дочірні вузли і на батьківський вузол відповідно. Якщо дочірній або батьківський вузол відсутні, відповідне поле містить значення nil. Єдиний вузол, покажчик р якого дорівнює nil, — це кореневий вузол дерева. Ключі в бінарному дереві пошуку зберігаються таким чином, щоб у будь-який момент задовольняти наступній властивості бінарного дерева пошуку. • Якщо х — вузол бінарного дерева пошуку, а вузол у знаходиться в лівому піддереві х, то • key [у] ≤ key [x]. Якщо вузол у знаходиться в правому піддереві х, то key [x] ≥ key [у].
6. 1 Вступ • Так, на мал. 6. 1 а ключ кореня дорівнює 5, ключі 2, 3 і 5, що не перевищують значення ключа в корені, знаходяться в його лівому піддереві, а ключі 7 і 8, що не менше, ніж ключ 5, — у його правому піддереві. Та ж властивість, як легко переконатися, виконується для кожного іншого вузла дерева. На мал. 6. 1 б показане дерево з тими ж вузлами, що має ті ж властивості, однак менш ефективне в роботі, оскільки його висота дорівнює 4, на відміну від дерева на мал. 6. 1 a, висота якого дорівнює 2. • Властивість бінарного дерева пошуку дозволяє нам вивести всі ключі, що знаходяться в дереві, у відсортованому порядку за допомогою простого рекурсивного алгоритму, називаного центрованим (симетричним) обходом дерева (inorder tree walk). Цей алгоритм одержав дану назву в зв'язку з тим, що ключ у корені піддерева виводиться між значеннями ключів лівого піддерева і правого піддерева. Маються й інші способи обходу, а саме — обхід у прямому порядку (preorder tree walk), при якому спочатку виводиться корінь, а потім — значення лівого і правого піддерева, і обхід у зворотному порядку (pos-torder tree walk), коли першими виводяться значення лівого і правого піддерева, а вже потім — кореня. Центрований обхід дерева Т реалізується процедурою Inorder_Tree_Walk(root [Т]):
6. 1 Вступ • • • INORDER_TREE_WALK(х) 1 if х ≠nil 2 then l. NORDER_TREE_WALK(left[x]) 3 print key[x] 4 l. NORDER_TREE_WALK(right[x])
6. 1 Вступ • Як приклад розглянете центрований обхід дерев, показаних на мал. 6. 1, - ви одержите в обох випадках той самий порядок ключів, а саме 2, 3, 5, 5, 7, 8. Коректність описаного алгоритму випливає безпосередньо з властивості бінарного дерева пошуку. Для обходу дерева потрібен час θ(n), оскільки після початкового виклику процедура викликається рівно два рази для кожного вузла дерева: один раз для його лівого дочірнього вузла, і один раз — для правого. Приведена далі теорема дає нам більш формальний доказ лінійності часу центрованого обходу дерева.
6. 1 Вступ • • Теорема 6. 1. Якщо х — корінь піддерева, у якому маємо n вузлів, то процедура IN 0 RDER_Tree_Walk(х) виконується за час θ (n). Доказ. Позначимо через Т(n) час, необхідний процедурі l. NORDER_TREE_WALK у випадку виклику з параметром, що представляє собою корінь дерева з n вузлами. При одержанні як параметр порожнього піддерева, процедурі потрібно невеликий постійний час для виконання перевірки х≠ NIL, так що Т (0) = c, де c — деяка невід’ємна константа. У випадку n > 0 будемо вважати, що процедура l. NORDER_TREE_WALK викликається один раз для піддерева з k вузлами, а другий — для піддерева з n — к — 1 вузлами. Таким чином, час роботи процедури складає Т (п) = Т(к) + Т (n — к — 1) + d, де d — деяка невідємна константа, у якій відбиває час, необхідне для виконання процедури без обліку рекурсивних викликів. Скористаємося методом підстановки, щоб показати, що Т (n) = θ(n), шляхом доказу того, що Т(п) = (c + d)n + с. При п = 0 одержуємо T (0) = (с + d) 0 + с = с. Якщо п > 0, то Т (n) = T(k) + T (n - к - 1) + d = = ((с + d) до + с) + ((с + d) (n - к - 1) + с) + d = (с + d)n + с- (с + d) + с + d = (с + d)n + с, що і завершує доказ.
6. 2 Процедура пошуку • Процедура пошуку починається з кореня дерева і проходить униз по дереву. Для кожного вузла х на шляху вниз його ключ key [x] порівнюється з переданим як параметр ключем к. Якщо ключі однакові, пошук завершується. Якщо к менше key [х], пошук продовжується в лівому піддереві х; якщо більше - те пошук переходить у праве піддерево. Так, на мал. 6. 2 для пошуку ключа 13 ми повинні пройти наступний шлях від кореня: 15 6 7 13. Вузли, що ми відвідуємо при рекурсивному пошуку, утворять спадний шлях від кореня дерева, так, що час роботи процедури TREE_SEARCH дорівнює О (h), де h — висота дерева. • Ту ж процедуру можна записати ітеративно, "розвертаючи" рекурсію в цикл while. На більшості комп'ютерів така версія виявляється більш ефективною.
TREE_SEARCH(x, k) 1 if x = NIL или к = кеу[x] 2 then return x 3 if к < кеу[х] 4 then return ТREE_SEARCH(left[x], к) 5 else return TREE_SEARCH(right[x], k)
6. 2 Процедура пошуку • Найбільш розповсюдженою операцією, виконуваної з бінарним деревом пошуку, є пошук у ньому визначеного ключа. Крім того, бінарні дерева пошуку підтримують такі запити, як пошук мінімального і максимального елемента, а також попереднього і наступного. У даному розділі ми розглянемо всі ці операції і покажемо, що усі вони можуть бути виконані в бінарному дереві пошуку висотою h за час О (h). • Пошук • Для пошуку вузла з заданим ключем у бінарному дереві пошуку використовується наступна процедура TREE SEARCH, що одержує як параметри покажчик на корінь бінарного дерева і ключ до, а повертає покажчик на вузол з цим ключем (якщо такий існує; у противному випадку повертається значення NIL).
6. 2 Процедура пошуку • • • • TREE_SEARCH(x, k) 1 if x = NIL або k = кеу[x] 2 then return x 3 if k < кеу[х] 4 then return ТREE_SEARCH(left[x], к) 5 else return TREE_SEARCH(right[x], k) Цю же процедуру можна записати ітерактивно, наприклад, ITERATIVE_TREE_SEARCH(x, k) 1 while x ≠ nil і k≠ кеу[х] 2 do if k < key[x] 3 then x ←left[x] 4 else x ←right[x] 5 return x
6. 2 Процедура пошуку • • • Елемент із мінімальним значенням ключа легко знайти, йдучи по покажчиках left від кореневого вузла доти, поки не зустрінеться значення NIL. Так, на мал. 6. 2, випливаючи по покажчиках left, ми пройдемо шлях 15 6 3 2 до мінімального ключа в дереві, рівного 2. От як виглядає реалізація описаного алгоритму: Tree_Minimum(x) 1 while left[x] ≠ nil 2 do x ←left[x] 3 return x Алгоритм пошуку максимального елемента дерева симетричний алгоритмові пошуку мінімального елемента: TREE_Ma. XIMUM(x) 1 while right[x] ≠ NIL 2 do x ←right[x] 3 return x Обидві представлені процедури знаходять мінімальний (максимальний) елемент дерева за час О (h), де h — висота дерева, оскільки, як і в процедурі TREE_SEARCH, послідовність вузлів, що перевіряються, утворить спадний шлях від кореня дерева. Попередній і наступний елементи.
6. 2 Процедура пошуку • Іноді, маючи вузол у бінарному дереві пошуку, потрібно визначити, який вузол чередує за ним у відсортованій послідовності, обумовленої порядком центрованого обходу бінарного дерева, і який вузол передує даному. Якщо всі ключі різні, наступним стосовно вузла х є вузол з найменшим ключем, великим key [x]. Структура бінарного дерева пошуку дозволяє нам знайти цей вузол навіть не виконуючи порівняння ключів. Приведена далі процедура повертає вузол, що випливає за вузлом х у бінарному дереві пошуку (якщо такий існує) і NIL, якщо х має найбільший ключ у бінарному дереві. • Tree_Successor(x) • 1 if right[x] ≠ NIL • 2 then return TREE_Mini. MUM(right[x]) • 3 у ←p[x] • 4 while у ≠ NIL і x = right[y] • 5 do x ← у • 6 y ← р[у] • 7 return у
6. 3 Вставка і видалення • Операції вставки і видалення приводять до внесення змін у динамічну множину, представлена бінарним деревом пошуку. Структура даних повинна бути змінена таким чином, щоб відбивати ці зміни, але при цьому зберегти властивість бінарних дерев пошуку. Як ми побачимо в цьому розділі, вставка нового елемента в бінарне дерево пошуку виконується відносно просто, однак з видаленням прийдеться повозитися. • Вставка. • Для вставки нового значення v у бінарне дерево пошуку Т ми скористаємося процедурою Tree_Insert. Процедура одержує як параметр вузол z, у якого key [z] = v, • left [z]= NIL і right [z] = NIL, після чого вона таким образі змінює Т и деякі полючи z, що z виявляється вставленим у відповідну позицію в дереві.
6. 3 Вставка і видалення • • • • Tree_Insert(T, z) 1 у ← NIL 2 х ← root[T] 3 while x ≠ NIL 4 do у ← х 5 if key[z] < key[x] 6 then x ← left[x] 7 else x ← right[x] 8 p[z) ← y 9 if у = NIL 10 then ← root[T] z Дерево Т — порожнє 11 else if key[z] < key[y] 12 then left[y] ← z 13 else right[y] ← z
6. 3 Вставка і видалення
6. 3 Вставка і видалення • Видалення • Процедура видалення даного вузла z з бінарного дерева пошуку одержує як аргумент покажчик на z. Процедура розглядає три можливі ситуації, показані на мал. 6. 4. Якщо у вузла z немає дочірніх вузлів (мал. 6. 4 а), те ми просто змінюємо його батьківський вузол р [z], заміняючи в ньому покажчик на z значенням nil. Якщо у вузла z тільки один дочірній вузол (мал. 6. 4 б), то ми видаляємо вузол z, створюючи новий зв'язок між батьківським і дочірнім вузлом вузла z. І нарешті, якщо у вузла z два дочірніх вузли (мал. 6. 4 в), то ми знаходимо наступний за ним вузол у, у якого немає лівого дочірнього вузла , забираємо його з позиції, де він знаходився раніше, шляхом створення нового зв'язку між його батьком і нащадком, і заміняємо їм вузол z.
6. 3 Вставка і видалення
6. 3 Вставка і видалення
6. 3 Вставка і видалення • • • • • Tree_Delete(T, z) 1 if left[z] = nil або right[z] = nil 2 then у ← z 3 else у ← Tree_Successor(z) 4 if left[y] ≠NIL 5 then x ← left[y] 6 else x ←right[y] 7 if x nil 8 then p[x] ← p[y] 9 if p[y] = nil 10 then root[T] ← x 11 else if у=left[p[y]] 12 then left[p[y]] ← x 13 else right([p[y]] ← x 14 if у ≠z 15 then key[z] ← key[y] 16 Копіювання супутніх даних у м 17 return у
6. 3 Вставка і видалення • У рядках 1 -3 алгоритм визначає видалений шляхом “склеювання" батька і нащадка вузол у. Цей вузол являє собою або вузол z (якщо у вузла z не більш одного дочірнього вузла), або вузол, що слідує за вузлом z (якщо в z два дочірніх вузли). Потім у рядках 4 -6 х привласнюється покажчик на дочірній вузол вузла у або значення nil, якщо в у немає дочірніх вузлів. Потім вузол у видаляється з дерева в рядках 7 -13 шляхом зміни покажчиків у р [у] і х. Це видалення ускладнюється необхідністю коректного відпрацьовування граничних умов (коли х дорівнює nil або коли у - кореневий вузол). І нарешті, у рядках 14 -16, якщо вилучений вузол у був наступним за z, ми перезаписуємо ключ z і супутні дані ключа супутніми даними у. Вилучений вузол у повертається в рядку 17, для того щоб процедура, що викликається могла при необхідності звільнити або використовувати займану їм пам'ять. Час роботи описаної процедури з деревом висотою h складає О (h).
6. 3 Вставка і видалення • Таким чином, у цьому розділі ми довели наступну теорему. • Теорема 6. 3 Операції вставки і видалення в бінарному дереві пошуку висоти h можуть бути виконані за час О (h).