lektsii_OP / T15
.pdf
Рис.16. Схема обходу дерева в прямому порядку
Наприклад,
Обхід, при якому проглядаються спочатку нащадки, а потім предки, називають обходом дерева у зворотному порядку. Схема обходу дерева в зворотному порядку
(рис. 17):
1)рекурсивно обробити ліве піддерево поточного піддерева;
2)рекурсивно обробити праве піддерево;
3)потім - вершину поточного піддерева.
Рис.17. Схема обходу дерева в зворотному порядку
Наприклад,
51
Обхід, при якому відвідується спочатку ліве піддерево, потім вузол, потім - праве піддерево називають симетричним обходом дерева. Схема симетричного обходу дерева (рис. 18):
1)рекурсивно обробити ліве піддерево поточного піддерева;
2)обробити вершину поточного піддерева;
3)рекурсивно обробити праве піддерево.
Рис.18. Схема симетричного обходу дерева
Наприклад,
Існує обхід в ширину, при якому вузли відвідуються рівень за рівнем (n-й рівень дерева - множина вузлів з висотою n). Кожен рівень обходиться зліва направо.
Обхід дерева слід проводити за рахунок послідовного виділення в дереві подібних найпростіших піддерев і застосуванням до кожного з них відповідного правила обходу. Виділення починається з кореневої вершини.
Будь-який спосіб обходу дерева можна реалізувати рекурсивною функцією.
Як приклад, розглянемо обхід наступного бінарного дерева з числовими компонентами:
52
Обхід в прямому порядку:
1)виділяємо піддерево 0-1-2
2)обробляємо його корінь - вершину 0
3)переходимо до лівого нащадка і виділяємо піддерево 1-3-4
4)обробляємо його корінь - вершину 1
5)виділяємо ліве піддерево 3 - * - * (тут * позначає порожнє посилання)
6)обробляємо його корінь - вершину 3
7)так як лівого нащадка немає, обробляємо праве піддерево
8)так як правого піддерева немає, повертаємося до піддерево 1-3-4
9)виділяємо піддерево 4-6-7
10)обробляємо його корінь - вершину 4
11)виділяємо ліве піддерево 6 - * - *
12)обробляємо його корінь - вершину 6
13)так як лівого нащадка немає, обробляємо праве піддерево
14)так як правого нащадка немає, то повертаємося до піддерево 4-6-7
15)виділяємо праве піддерево 7 - * - *
16)обробляємо його корінь - вершину 7
17)так як лівого піддерева немає, обробляємо праве піддерево
18)так як правого піддерева немає, то повертаємося до піддерево 4-6-7
19)так як піддерево 4-6-7 оброблено, то повертаємося до піддерево 1-3-4
20)так як піддерево 1-3-4 оброблено, повертаємося до піддерево 0-1-2
21)виділяємо праве піддерево 2 - * -5
22)обробляємо його корінь - вершину 2
23)так як лівого нащадка немає, обробляємо правого нащадка
24)виділяємо піддерево 5-8-9
25)обробляємо його корінь - вершину 5
26)виділяємо ліве піддерево 8 - * - *
27)обробляємо його корінь - вершину 8
28)так як лівого піддерева немає, обробляємо праве піддерево
29)так як правого піддерева немає, то повертаємося до піддерево 5-8-9
30)виділяємо праве піддерево 9 - * - *
31)обробляємо його корінь - вершину 9
32)так як лівого піддерева немає, обробляємо праве піддерево
33)так як правого піддерева немає, то повертаємося до піддерево 5-8-9
34)так як піддерево 5-8-9 оброблено, то повертаємося до піддерево 2 - * -5
35)так як піддерево 2 - * -5 оброблено, то повертаємося до піддерево 0-1-2
36)так як піддерево 0-1-2 повністю оброблено, то обхід закінчений
Упідсумку отримуємо наступний порядок обходу вершин: 0-1-3-4-6-7-2-5-8-9.
1) |
піддерево |
9) |
вершина 4 |
17) |
піддерево |
25) |
піддерево |
|
0-1-2 |
|
|
|
0-1-2 |
|
9 - * - * |
2) |
вершина |
10) |
піддерево |
18) |
піддерево |
26) |
вершина |
|
0 |
|
6 - * - * |
|
2 - * -5 |
|
9 |
3) |
піддерево |
11) |
вершина 6 |
19) |
вершина 2 |
27) |
піддерево |
|
1-3-4 |
|
|
|
|
|
5-8-9 |
4) |
вершина 1 |
12) |
піддерево |
20) |
піддерево |
28) |
піддерево |
|
|
|
4-6-7 |
|
5-8-9 |
|
2 - * -5 |
|
|
|
53 |
|
|
|
|
5) |
ліве піддерево |
13) |
піддерево |
21) |
вершина 5 |
29) піддерево |
|
3 - * - * |
|
7 - * - * |
|
|
0-1-2 |
6) |
вершина 3 |
14) |
вершина 7 |
22) |
піддерево |
|
|
|
|
|
|
8 - * - * |
|
7) |
піддерево |
15) |
піддерево |
23) |
вершина 8 |
|
|
1-3-4 |
|
4-6-7 |
|
|
|
8)піддерево 16) піддерево 24) піддерево
4-6-7 |
1-3-4 |
5-8-9 |
У більш короткому записі симетричний обхід дає наступні результати:
1) |
піддерево |
6) |
піддерево |
11) |
вершина 7 |
16) |
піддерево |
|
0-1-2 |
|
4-6-7 |
|
|
|
8 - * - * |
2) |
піддерево |
7) |
піддерево |
12) |
вершина 0 |
17) |
вершина 8 |
|
1-3-4 |
|
6 - * - * |
|
|
|
|
3) |
піддерево |
8) |
вершина 6 |
13) |
піддерево |
18) |
вершина 5 |
|
3 - * - * |
|
|
|
2 - * -5 |
|
|
4) |
вершина 3 |
9) |
вершина 4 |
14) |
вершина 2 |
19) |
піддерево |
|
|
|
|
|
|
|
9 - * - * |
5) |
вершина 1 |
10) |
піддерево |
15) |
піддерево |
20) |
вершина 9 |
|
|
|
7 - * - * |
|
5-8-9 |
|
|
Разом: 3-1-6-4-7-0-2-8-5-9.
Аналогічно, обхід в зворотному порядку дає:
1) |
піддерево |
5) |
вершина 6 |
9) |
піддерево |
13) |
вершина 5 |
|
0-1-2 |
|
|
|
2 - * -5 |
|
|
2) |
піддерево |
6) |
вершина 7 |
10) |
піддерево |
14) |
вершина 2 |
|
1-3-4 |
|
|
|
5-8-9 |
|
|
3) |
вершина 3 |
7) |
вершина 4 |
11) |
вершина 8 |
15) |
вершина 0 |
4) |
піддерево |
8) |
вершина 1 |
12) |
вершина 9 |
|
|
|
4-6-7 |
|
|
|
|
|
|
Разом: 3-6-7-4-1-8-9-5-2-0.
Як видно, результуюча послідовність вершин істотно залежить від правила обходу.
Іноді використовуються різновиди трьох основних правил, наприклад - обхід в обернено-симетричному порядку: праве піддерево - корінь - ліве піддерево.
Різні правила обходу часто використовуються для виведення структури дерева в наочному графічному вигляді. Наприклад, для розглянутого вище дерева з десятьма вершинами застосування різних правил обходу дозволяє отримати наступні представлення дерева:
54
Прямий обхід |
Зворотно-симетричний обхід |
Симетричний обхід |
З цих прикладів видно, що наявність декількох правил обходу дерева цілком обгрунтована, і в кожній ситуації треба вибирати підходяче правило.
Також зручно використовувати різні правил обходу при аналізі виразів. У результаті обходу дерева зверху вниз утворюється префіксна форма виразу, при обході знизу вверх - постфіксна форма, а при обході зліва направо - інфіксна форма.
Враховуючи рекурсивний характер правил обходу, програмна їх реалізація найбільш просто може бути виконана за допомогою рекурсивних підпрограм. Кожен рекурсивний виклик відповідає за обробку свого поточного піддерева.
З наведених вище прикладів видно, що після повної обробки поточного піддерева відбувається повернення до піддерева більш високого рівня, а для цього треба запам'ятовувати і надалі відновлювати адресу кореневої вершини цього піддерева. Рекурсивні виклики дозволяють виконати це запам'ятовування і відновлення автоматично, якщо описати адресу кореневої вершини піддерева як формальний параметр рекурсивної підпрограми.
Кожен рекурсивний виклик, насамперед, повинен перевірити передану адресу на NULL. Якщо ця адреса дорівнює NULL, то чергове оброблюване піддерево є порожнім і його обробка не потрібна, тому просто відбувається повернення з рекурсивного виклику. В іншому випадку, у відповідності до правила обходу, проводиться або обробка вершини, або рекурсивний виклик для обробки лівого чи правого піддерева.
Рекурсивна реалізація обходу в прямому напрямку:
void forward (TREE * current)
{if (current != NULL)
{<обробка кореневої вершини current>; forward (current->left);
forward (current->right);
}
}
Початковий виклик рекурсивної підпрограми здійснюється в головній програмі. В якості стартової вершини задається адреса кореневої вершини дерева:
forward (root).
55
Якщо вважати, що функції обходу з іменами symmetric і back відрізняються тільки порядком проходження трьох основних інструкцій в тілі умовного оператора, то реалізація симетричного проходу має вид:
symmetric (current -> left);
<обробка кореневої вершини current>; symmetric (current -> right);
Для зворотного проходу:
back (current -> left); back (current -> right);
<обробка кореневої вершини current>;
Досить легко реалізувати нерекурсивний варіант процедур обходу, якщо врахувати, що рекурсивні виклики та повернення використовують стековий принцип роботи. Наприклад, розглянемо схему реалізації нерекурсивного симетричного обходу. У відповідності з даним правилом спочатку треба обробити всі ліві нащадки, тобто спуститься вліво максимально глибоко. Кожне просування вниз до лівого нащадку призводить до запам'ятовування в стеку адреси колишньої кореневої вершини. Тим самим для кожної вершини в стеку запам'ятовується шлях до цієї вершини від кореня дерева.
Звернення до рекурсивної функції для обробки лівого нащадка треба замінити розміщенням в стек адреси поточної кореневої вершини і переходом до лівого нащадку цієї вершини. Обробка правого нащадка полягає у витяганні із стека адреси деякої вершини і переході до її правого нащадка.
Для нерекурсивного обходу дерева необхідно оголосити допоміжну структуру даних - стек. В інформаційній частині елементів стека повинні зберігатися адреси вузлів цього дерева, тому її треба описати за допомогою відповідного посилального типу.
Схематично нерекурсивний симетричний обхід можна представити наступним чином:
current = root; |
// починаємо з кореневої вершини дерева |
|
stop = 0; |
// |
допоміжна змінна |
while (stop == 0) |
|
// основний цикл обходу |
{ while (current!= NULL) |
// обробка лівих нащадків |
|
{ <занести current в стек>; current = current-> left;
}
if (<стек порожній>)
stop = 1; // обхід закінчений else
{<витягти з стека адресу і привласнити його current>; <обробка вузла current>;
current = current->right;
}
}
56
Бінарні дерева пошуку
На основі процедур обходу легко можна реалізувати пошук в дереві вершини із заданим інформаційним значенням. Для цього кожна поточна вершина перевіряється на збіг із заданим значенням і в разі успіху відбувається завершення обходу.
Основна сфера використання бінарних дерев – реалізація алгоритмів бінарного пошуку. Такі дерева називають бінарними деревами пошуку.
Двійкове дерево називається деревом пошуку, якщо для кожного вузла дерева всі значення в його лівому піддереві менші за значення цього вузла, а всі значення в його правому піддереві більші значення вузла. Наприклад,
Дерева пошуку є однією з найбільш ефективних структур побудови впорядкованих даних. Як відомо, у впорядкованому масиві дуже ефективно реалізується пошук, але дуже важко виконати додавання і видалення елементів. Навпаки, в упорядкованому списку легко реалізується додавання і видалення елементів, але не ефективна реалізація пошуку через необхідність послідовного перегляду всіх елементів, починаючи з першого. Дерева пошуку дозволяють об'єднати переваги масивів і лінійних списків: легко реалізується додавання і видалення елементів, а також ефективно виконується пошук.
Алгоритм пошуку в дереві пошуку дуже простий. Починаючи з кореневої вершини для кожного поточного піддерева треба виконати наступні кроки:
порівняти ключ вершини із заданим значенням;
якщо задане значення менше ключа вершини, перейти до лівого нащадку, інакше перейти до правого піддерева.
Пошук припиняється при виконанні однієї з двох умов:
або якщо знайдений шуканий елемент;
або якщо треба продовжувати пошук в порожньому поддереві, що є ознакою відсутності шуканого елемента.
Слід зазначити, що пошук дуже легко можна реалізувати простим циклом, без використання рекурсії:
сurrent = root; // починаємо пошук з кореня дерева stop = 0;
while (сurrent != NULL && stop == 0)
57
if (x < сurrent ->info) сurrent = сurrent ->left; else
if (x > current ->info) сurrent = сurrent ->right; else stop = 1;
Трохи складніше реалізується операція додавання нового елемента в дерево пошуку. Перш за все, треба знайти відповідне місце для нового елемента, тому додавання нерозривно пов'язано з процедурою пошуку. Будемо вважати, що в дерево можуть додаватися елементи з однаковими ключами, і для цього з кожною вершиною зв'яжемо лічильник числа появи цього ключа. У процесі пошуку може виникнути одна з двох ситуацій:
знайдена вершина із заданим значенням ключа і в цьому випадку просто збільшується лічильник;
пошук треба продовжувати по порожньому посиланню, що говорить про відсутність в дереві шуканої вершини, більше того, тим самим визначається місце в дереві для розміщення нової вершини.
Саме додавання включає наступні кроки:
1)виділення пам'яті для нової вершини;
2)формування інформаційної складової;
3)формування двох порожніх посилальних полів на майбутніх нащадків;
4)формування в батьківській вершині лівого або правого посилального поля - адреси нової вершини.
Тут тільки остання операція викликає деяку складність, оскільки для доступу до посилального поля батьківського вершини треба знати її адресу. Ситуація аналогічна додаванню елемента в лінійний список перед заданим, коли для відстеження елемента-попередника при проході за списком використовувався додатковий покажчик. Цей же прийом можна використовувати і для дерев, але є більш елегантне рішення - рекурсивний пошук із додаванням нової вершини при необхідності. Кожен рекурсивний виклик відповідає за обробку чергової вершини дерева, починаючи з кореня, а вся послідовність вкладених викликів дозволяє автоматично запам'ятовувати шлях від кореня до будь-якої поточної вершини. Процедура пошуку повинна мати формальний параметр-змінну посилального типу, який відстежує адресу поточної вершини дерева і як тільки ця адреса стає порожньою, створюється нова вершина і її адреса повертається в викликаючу процедуру, тим самим автоматично формуючи необхідне посилання у батьківської вершини.
У порівнянні із додаванням, видалення реалізується більш складним алгоритмом, оскільки вершина, що додається завжди є термінальною, а видалятися може будьяка, у тому числі і нетермінальна. При цьому може виникати кілька різних ситуацій:
вузол, що видаляється, не має нащадків, тобто є листком дерева;
вузол, що видаляється, має одного нащадка;
вузол, що видаляється, має двох нащадків.
Коли вузол, що видаляється, є листком дерева, слід звільнити ділянку динамічної пам’яті, яку цей вузол займав, та присвоїти значення NULL покажчикові на даний
58
вузол (рис. 19 (а)). Якщо видаляється вузол, який має одного нащадка, то покажчику на цей вузол слід присвоїти адресу його нащадка і звільнити пам’ять, яку даний вузол займав (рис. 19 (б)).
а) |
б) |
Рис. 19. Схеми видалення вузлів дерева Якщо вузол, що видаляється, має двох нащадків, на його місце слід переставити
інший вузол дерева так, щоб не порушувалася властивість впорядкованості ключів. Тобто, у цьому випадку потрібно знайти підходяще піддерево, яке можна було б вставити на місце вершини, що видаляється. Таке піддерево завжди існує: це або самий правий елемент лівого піддерева, або самий лівий елемент правого піддерева. У першому випадку для досягнення цієї ланки необхідно перейти в наступну вершину по лівій гілці, а потім переходити в чергові вершини тільки по правій гілці до тих пір, поки черговий вказівник не буде дорівнює NULL. У другому випадку необхідно перейти в наступну вершину по правій гілці, а потім переходити в чергові вершини тільки по лівій гілці до тих пір, поки черговий вказівник не стане NULL. Такі підходящі ланки можуть мати не більше однієї гілки. Нижче (рис. 20) схематично зображено виключення з дерева вершини з ключем 50:
Рис. 20. Приклад видалення вузлів дерева
Збалансовані дерева
Ефективне використання дерев на практиці часто вимагає управління ростом дерева для усунення крайніх випадків, коли дерево вироджується в лінійний список і тим самим втрачає всю свою привабливість (з обчислювальної точки зору).
59
У цьому сенсі збалансоване дерево повністю виправдовує свою назву, оскільки вершини в ньому розподіляються найбільш рівномірно і тим самим таке дерево має мінімально можливу висоту.
Бінарне дерево називається збалансованим, якщо для кожної вершини число вершин у лівому і правому її піддереві відрізняється не більше ніж на одиницю. Дана умова має виконуватися для всіх вершин дерева.
Ідеально збалансовані дерева є ресурсоємними для балансування, тому зазвичай балансування здійснюється для так званих АВЛ-дерев.
AVL-збалансоване дерево —дерево, у якого для кожної вершини різниця між висотами лівого та правого піддерев не перевищує одиниці.
Кожне ідеально збалансоване дерево є також AVL-збалансованим, але не навпаки.
Дерево (піддерево), яке потребує балансування, балансується за допомогою операції обертання вліво чи вправо (переважно при видаленні або додаванні нових елементів).
Збалансоване дерево легко будується, якщо заздалегідь відома кількість вершин n в цьому дереві. У цьому випадку таке дерево можна побудувати за допомогою наступного рекурсивного алгоритму:
1)взяти першу по порядку вершину в якості кореневої;
2)знайти кількість вершин у лівих і правих піддеревах: nl = n/2; nr = n-nl-1;
3)аналогічним чином побудувати ліве піддерево з nl вершинами (поки не отримаємо nl = 0)
4)аналогічним чином побудувати праве піддерево з nr вершинами (поки не
отримаємо nr=0)
Природно, що реалізація рекурсивного алгоритму найбільш просто виконується у вигляді рекурсивної підпрограми. При цьому між цією процедурою і процедурами обходу є одна принципова відмінність: процедури обходу лише використовують існуючу структуру дерева, не змінюючи її, і тому їх формальні параметри є лише вхідними, тоді як процедура побудови збалансованого дерева повинна створювати вершини і щоразу повертати в викликаючу її підпрограму адресу чергової створеної вершини. Тому формальний параметр посилального типу повинен бути оголошений як параметр-змінна. Крім того, другий формальний параметр-значення приймає число вершин у поточному піддереві, яке будується.
Наприклад,
60
