Яке саме балансування двійкового дерева ви б рекомендували?


18

Я вивчаю Хаскелл і як вправу роблю бінарні дерева. Склавши звичайне бінарне дерево, я хочу адаптувати його до самоврівноваження. Так:

  • Що найефективніше?
  • Що найпростіше здійснити?
  • Що найчастіше використовується?

Але важливо, що ви рекомендуєте?

Я припускаю, що це належить тут, оскільки воно відкрите для дебатів.


З точки зору ефективності та простоти реалізації загальна ефективність чітко визначена, але для вашої реалізації я вважаю, що найкраще було б реалізувати стільки, скільки зможете знайти, а потім повідомити нам, що найкраще працює ...
glenatron

Відповіді:


15

Я б порекомендував вам почати або з Червоно-Чорного дерева , або з AVL-дерева .

Червоно-чорне дерево швидше вставляє, але дерево AVL має невеликий край для пошуку. Дерево AVL, мабуть, трохи легше втілити, але не настільки багато, що спирається на власний досвід.

Дерево AVL гарантує збалансованість дерева після кожного вставки або видалення (жодне піддерево не має коефіцієнта балансу більше 1/1, в той час як червоно-чорне дерево забезпечує збалансованість дерева в будь-який час.


1
Особисто мені червоне-чорне вставлення простіше, ніж AVL. Причина - через (недосконалу) аналогію B-деревам. Вкладиші вигадливі, але видалення - це зло (стільки випадків слід розглядати). Насправді у мене більше немає власної реалізації C ++ червоно-чорного видалення - я видалив її, коли зрозумів (1) Я ніколи не використовував її - кожен раз, коли мені хотілося видалити, я видаляв декілька елементів, тому перетворював з дерева на список, видаліть зі списку, потім перетворіть назад на дерево, і (2) воно було зламане в будь-якому випадку.
Steve314

2
@ Steve314, червоно-чорні дерева легше, але ви не змогли зробити реалізацію, яка працює? Як виглядають дерева AVL?
dan_waterworth

@dan_waterworth - Я ще не здійснив реалізацію навіть методом вставки, який ще працює - маю зауваження, розумію основний принцип, але ніколи не отримав правильного поєднання мотивації, часу та впевненості. Якщо я просто хотів, щоб працювали версії, це просто копіювати псевдокод-з-підручника та перекладати (і не забувайте, що C ++ має стандартні бібліотечні контейнери), але де в цьому весело?
Steve314

BTW - Я вважаю (але не можу надати посилання), що досить популярний підручник включає помилкову реалізацію одного з алгоритмів збалансованого двійкового дерева - не впевнений, але це може бути червоно-чорне видалення. Тож це не тільки я ;-)
Steve314

1
@ Steve314, я знаю, дерева можуть бути по-справжньому складними в імперативній мові, але дивно, що їх реалізація в Haskell була легким вітерцем. Я написав звичайне дерево AVL, а також 1D просторовий варіант у вихідні, і вони мають лише близько 60 рядків.
dan_waterworth

10

Я б розглядав альтернативу, якщо ви добре з рандомізованими структурами даних: Пропустити списки .

З точки зору високого рівня, це структура дерева, за винятком того, що вона реалізується не як дерево, а як список з декількома шарами посилань.

Ви отримаєте O (log N) вставки / пошуки / видалення, і вам не доведеться мати справу з усіма цими хитрими справами відновлення балансу.

Я ніколи не думав реалізовувати їх у функціональній мові, але на сторінці вікіпедії не відображається жодна інформація, тому це може бути непросто (wrt до незмінності)


Мені дуже подобається пропускати списки, і я реалізував їх раніше, хоча і не функціональною мовою. Думаю, я спробую їх після цього, але зараз я перебуваю на деревах, що самоврівноважуються.
dan_waterworth

Також люди часто використовують пропускні списки для одночасних структур даних. Можливо, краще, ніж примушувати незмінність, використовувати паралельні примітиви haskell (наприклад, MVar або TVar). Хоча це не дуже навчить мене писати функціональний код.
dan_waterworth

2
@ Fanatic23, пропускний список не є АДТ. ADT - це набір або асоціативний масив.
dan_waterworth

@dan_waterworth мені погано, ти прав.
Fanatic23

5

Якщо ви хочете розпочати відносно легку структуру (і AVL-дерева, і червоно-чорні дерева химерно), одним із варіантів є треп - названий як комбінація "дерево" та "купа".

Кожен вузол отримує значення "пріоритет", часто випадковим чином присвоюється тому, як створюється вузол. Вузли розташовуються на дереві так, щоб дотримуватися впорядкування ключів, і таким чином дотримуватися впорядкованих впорядкування значень пріоритетів. Порядок, що нагадує купу, означає, що обидва батьки мають нижчі пріоритети, ніж батьки.

EDIT видалено "в межах ключових значень" вище - пріоритет та упорядкування ключів застосовуються разом, тому пріоритет є важливим навіть для унікальних ключів.

Це цікаве поєднання. Якщо ключі унікальні, а пріоритети - унікальні, існує унікальна структура дерева для будь-якого набору вузлів. Незважаючи на це, вставки та видалення є ефективними. Строго кажучи, дерево може бути неврівноваженим до того моменту, коли воно фактично пов'язаний список, але це вкрай малоймовірно (як у стандартних бінарних дерев), у тому числі для звичайних випадків, таких як ключі, вставлені в порядку (на відміну від стандартних бінарних дерев).


1
+1. Treaps - це мій особистий вибір, я навіть писав допис у блозі про те, як вони реалізовані.
П Швед

5

Що найефективніше?

Неясне і важко відповісти. Комп'ютерні складності всі чітко визначені. Якщо це означає ефективність, справжніх дебатів немає. Дійсно, всі хороші алгоритми мають докази та фактори складності.

Якщо ви маєте на увазі "час запуску" або "використання пам'яті", вам потрібно буде порівняти фактичні реалізації. Тоді мова, час роботи, ОС та інші фактори вступають у гру, що ускладнює відповідь на питання.

Що найпростіше здійснити?

Неясне і важко відповісти. Деякі алгоритми можуть здатися вам складними, але для мене банальними.

Що найчастіше використовується?

Неясне і важко відповісти. Спочатку є "ким?" частина цього? Тільки Haskell? Що з C або C ++? По-друге, існує проблема фірмового програмного забезпечення, коли ми не маємо доступу до джерела, щоб зробити опитування.

Але важливо, що ви рекомендуєте?

Я припускаю, що це належить тут, оскільки воно відкрите для дебатів.

Правильно. Оскільки ваші інші критерії не дуже корисні, це все, що ви збираєтеся отримати.

Ви можете отримати джерело для великої кількості алгоритмів дерев. Якщо ви хочете чогось навчитися, ви можете просто реалізувати кожен, кого ви можете знайти. Замість того, щоб просити "рекомендацію", просто збирайте кожен алгоритм, який ви можете знайти.

Ось список:

http://en.wikipedia.org/wiki/Self-balancing_binary_search_tree

Визначено шість популярних. Почніть з тих.


3

Якщо вас цікавлять дерева Splay, є простіша версія тих, які, на мою думку, були вперше описані у статті Аллена та Манро. Він не має однакових гарантій продуктивності, але дозволяє уникнути ускладнень у взаємодії з "zig-zig" проти "zig-zag" зрівноваження.

В основному, під час пошуку (включаючи пошук точки вставки або вузла для видалення), знайдений вузол повертається безпосередньо до кореня, знизу вгору (наприклад, коли функція рекурсивного пошуку виходить). На кожному кроці ви вибираєте одну ліву або праву обертання залежно від того, дитина, якій ви хочете підняти ще один крок до кореня, - це права дитина чи ліва дитина (якщо я правильно пам'ятаю свої напрямки обертання, це відповідно).

Як і Splay дерева, ідея полягає в тому, що останнім часом елементи, які отримують доступ, завжди знаходяться біля кореня дерева, тому швидкий доступ знову. Якщо простіше, ці Аллен-Манро обертаються до кореневих дерев (як я їх називаю - не знаю офіційної назви), можуть бути швидшими, але вони не мають тієї ж амортизованої гарантії продуктивності.

Одне - оскільки ця структура даних за визначенням мутує навіть для операцій з пошуку, ймовірно, вона повинна бути реалізована монально. IOW, можливо, це не дуже добре підходить для функціонального програмування.


Ескізи трохи дратують, враховуючи те, що вони змінюють дерево навіть при знаходженні. Це було б досить болісно в багатопотокових середовищах, що є однією з великих мотивацій для використання функціональної мови, наприклад Haskell, в першу чергу. Знову ж таки, я ніколи раніше не використовував функціональні мови, тому, можливо, це не є фактором.
Швидкий Джо Сміт

@Quick - залежить від того, як ти маєш намір використовувати дерево. Якщо ви використовували його в справжньому коді функціонального стилю, ви або кинете мутацію на кожну знахідку (зробивши дерево Splay трохи нерозумним), або в кінцевому підсумку дублюєте значну частину двійкового дерева при кожному пошуку, і слідкуйте за тим, з яким деревом ви працюєте в міру розвитку вашої роботи (причина, ймовірно, для використання монадичного стилю). Це копіювання може бути оптимізовано компілятором, якщо ви більше не посилаєтесь на старе дерево після створення нового (подібні припущення поширені у функціональному програмуванні), але це може не зробити.
Steve314

Жоден підхід не звучить вартих зусиль. Потім знову ж таки здебільшого не виконуйте суто функціональних мов.
Швидкий Джо Сміт

1
@Quick - копіювання дерева - це те, що ви зробите для будь-якої структури даних дерева чистою функціональною мовою для мутації алгоритмів, таких як вставки. По-перше, код не буде настільки відмінним від імперативного коду, який робить оновлення на місці. Різниці вже вирішено, імовірно, для незбалансованих бінарних дерев. Поки ви не намагаєтеся додати батьківські посилання до вузлів, дублікати будуть ділити загальні підкреслення як мінімум, і глибока оптимізація в Haskell є досить жорсткою, якщо не ідеальною. Я в принципі сам проти Haskell, але це не обов'язково проблема.
Steve314

2

Дуже просте збалансоване дерево - це АА . Інваріант простіший і, таким чином, легший в реалізації. Через свою простоту, його продуктивність як і раніше хороша.

Як вдосконалене вправу, ви можете спробувати використовувати GADT для реалізації одного з варіантів збалансованих дерев, інваріант яких застосовується за типом системи типу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.