Відповіді:
Сперечатися з приводу продуктивності бінарних дерев безглуздо - це не структура даних, а сімейство структур даних, всі з різними характеристиками продуктивності. Хоча це правда, що неврівноважені бінарні дерева працюють набагато гірше, ніж самоврівноважувані бінарні дерева для пошуку, є багато бінарних дерев (наприклад, бінарних спроб), для яких "балансування" не має значення.
map
та set
об'єкти в бібліотеках багатьох мов.Причина того, що двійкові дерева використовуються частіше, ніж n-арі дерева для пошуку, полягає в тому, що n-ary-дерева є складнішими, але зазвичай не забезпечують реальної переваги швидкості.
У (врівноваженому) двійковому дереві з m
вузлами для переміщення з одного рівня на інший потрібне одне порівняння, і є log_2(m)
рівні для загального log_2(m)
порівняння.
Навпаки, для n-ary-дерева потрібно буде log_2(n)
порівняння (використовуючи двійковий пошук) для переходу на наступний рівень. Оскільки існують log_n(m)
загальні рівні, пошук вимагатиме log_2(n)*log_n(m)
= log_2(m)
порівняння всього. Отже, хоча n-ary дерева є складнішими, вони не дають переваги в плані загального порівняння.
(Однак n-ary дерева все ще корисні в нішевих ситуаціях. Прикладами, які відразу приходять на думку, є квадратичні дерева та інші дерева, що розділяють простір, де поділ простору з використанням лише двох вузлів на рівні зробить логіку надмірно складною; і B-дерева, що використовуються у багатьох базах даних, де обмежуючим фактором є не кількість порівнянь на кожному рівні, а скільки вузлів можна завантажувати з жорсткого диска відразу)
Коли більшість людей говорять про двійкові дерева, вони частіше, ніж не замислюються про дерева бінарного пошуку , тому я спершу висвітлю це.
Неврівноважене дерево бінарного пошуку насправді корисне для навчання більше, ніж навчання учнів. Це тому, що, якщо дані не надходять у відносно випадковому порядку, дерево може легко перерости у найгірший вигляд, який є пов'язаним списком, оскільки прості бінарні дерева не врівноважені.
Хороший випадок: колись мені довелося виправити якесь програмне забезпечення, яке завантажувало його дані у бінарне дерево для маніпуляцій та пошуку. Він записував дані у відсортованому вигляді:
Alice
Bob
Chloe
David
Edwina
Frank
щоб, прочитавши його ще раз, опинилося наступне дерево:
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
що є виродженою формою. Якщо ви підете шукати Френка на цьому дереві, вам доведеться пошукати всі шість вузлів, перш ніж його знайти.
Бінарні дерева стають справді корисними для пошуку, коли ви їх балансуєте. Це передбачає обертання під дерев через їхній кореневий вузол, щоб різниця висот між будь-якими двома під деревами була меншою або дорівнює 1. Якщо додати ці імена над одним по одному до збалансованого дерева, ви отримаєте таку послідовність:
1. Alice
/ \
= =
2. Alice
/ \
= Bob
/ \
= =
3. Bob
_/ \_
Alice Chloe
/ \ / \
= = = =
4. Bob
_/ \_
Alice Chloe
/ \ / \
= = = David
/ \
= =
5. Bob
____/ \____
Alice David
/ \ / \
= = Chloe Edwina
/ \ / \
= = = =
6. Chloe
___/ \___
Bob Edwina
/ \ / \
Alice = David Frank
/ \ / \ / \
= = = = = =
Насправді ви можете бачити цілі піддерева, що обертаються ліворуч (на кроках 3 та 6), коли записи додаються, і це дає вам збалансоване двійкове дерево, у якому найгірший випадок пошуку, O(log N)
а не той O(N
), який дає вироджена форма. Ні в якому разі найвищий NULL ( =
) не відрізняється від найнижчого більш ніж на один рівень. І, в кінцевому дереві вище, ви можете знайти Франк лише дивлячись на трьох вузлах ( Chloe
, Edwina
і, нарешті, Frank
).
Звичайно, вони можуть стати ще кориснішими, якщо ви зробите їх збалансованими багатосторонніми деревами, а не двійковими тресами. Це означає, що кожен вузол містить більше одного елемента (технічно вони містять N елементів та покажчиків N + 1, двійкове дерево - особливий випадок одностороннього багатостороннього дерева з 1 елементом та 2 вказівниками).
З тристороннім деревом ви закінчите:
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
Зазвичай це використовується для підтримки ключів для індексу елементів. Я написав програмне забезпечення для бази даних, оптимізоване для обладнання, де вузол має саме розмір дискового блоку (скажімо, 512 байт), і ви кладете стільки клавіш, скільки зможете в один вузол. Ці покажчики в даному випадку були фактично запис числа в фіксованої довжини, запис файлу прямого доступу окремо від індексного файлу (так номер запису X
може бути знайдений тільки прагне X * record_length
).
Наприклад, якщо покажчики складають 4 байти, а розмір ключа - 10, кількість клавіш у 512-байтовому вузлі - 36. Це 36 клавіш (360 байт) і 37 покажчиків (148 байт) на загальну суму 508 байт з 4 байти витрачено на вузол.
Використання багатосторонніх клавіш вносить складність двофазного пошуку (багатосторонній пошук для пошуку правильного вузла в поєднанні з невеликим послідовним (або лінійним двійковим) пошуком для пошуку правильної клавіші у вузлі), але перевага в робити менше вводу / виводу диска більше, ніж компенсує це.
Я не бачу причин робити це для структури пам’яті, вам краще дотримуватися збалансованого бінарного дерева, а ваш код буде простий.
Також майте на увазі, що переваги O(log N)
над O(N)
справді не з’являються, коли ваші набори даних невеликі. Якщо ви використовуєте багатостороннє дерево для зберігання п’ятнадцяти людей у своїй адресній книзі, це, ймовірно, зайве. Переваги приходять, коли ви зберігаєте щось на зразок кожного замовлення від своїх ста тисяч клієнтів протягом останніх десяти років.
Вся суть нотації big-O полягає в тому, щоб вказати на те, що відбувається в міру N
наближення до нескінченності. Деякі люди можуть не погодитися, але навіть нормально використовувати сортування бульбашок, якщо ви впевнені, що набори даних залишаться нижче певного розміру, доки нічого іншого не буде легко :-)
Що стосується інших видів використання для двійкових дерев, їх дуже багато, таких як:
З огляду на те, скільки пояснень я створив для дерев пошуку, я дуже схильний детально описувати інші деталі, але цього має бути достатньо для їх дослідження, якщо ви хочете.
Організація коду Морзе - це бінарне дерево.
Бінарне дерево - це структура даних про дерево, в якій кожен вузол має щонайбільше два дочірні вузли, зазвичай розрізняють як "лівий" та "правий". Вузли з дітьми є батьківськими вузлами, а дочірні вузли можуть містити посилання на батьків. Поза деревом часто є посилання на "кореневий" вузол (родоначальник усіх вузлів), якщо він існує. Будь-який вузол у структурі даних можна досягти, запустившись у кореневому вузлі та неодноразово дотримуючись посилань на ліву або праву дочірню. У двійковому дереві ступінь кожного вузла - максимум два.
Двійкові дерева корисні, тому що, як ви бачите на малюнку, якщо ви хочете знайти який-небудь вузол на дереві, вам слід подивитися максимум 6 разів. Наприклад, якщо ви хотіли знайти вузол 24, ви б почали з кореня.
Цей пошук показано нижче:
Видно, що ви можете виключити половину вузлів усього дерева на першому проході. і половина лівого піддерев’я на другому. Це робить дуже ефективними пошуки. Якби це було зроблено на 4 мільярдах елементів, вам доведеться шукати максимум 32 рази. Тому, чим більше елементів міститься в дереві, тим ефективнішим може бути пошук.
Видалення може стати складним. Якщо у вузлі 0 або 1 дитина, тоді просто перенести деякі покажчики, щоб виключити той, який потрібно видалити. Однак ви не можете легко видалити вузол з двома дітьми. Отже, ми робимо скорочення. Скажімо, ми хотіли видалити вузол 19.
Оскільки намагатися визначити, куди рухати лівий і правий покажчики, непросто, ми знаходимо такий, щоб замінити його. Ми йдемо до лівого під дерева, і йдемо так само праворуч, як можемо. Це дає нам наступне найбільше значення вузла, який ми хочемо видалити.
Тепер ми копіюємо весь вміст 18, за винятком лівого та правого покажчиків, та видаляємо початковий 18 вузол.
Для створення цих зображень я реалізував дерево AVL, дерево, що самоврівноважує, так що в будь-який момент часу дерево має щонайменше один рівень різниці між вузлами листя (вузли без дітей). Це не дозволяє дереву перекоситись і підтримує максимальний O(log n)
час пошуку, при цьому витрачається трохи більше часу, необхідного для вставки та видалення.
Ось зразок, який показує, як моє дерево AVL зберегло себе максимально компактно і врівноважено.
У відсортованому масиві пошук все ще займеться O(log(n))
, як і дерево, але випадкове вставлення та видалення займе O (n) замість дерева O(log(n))
. Деякі контейнери STL використовують ці експлуатаційні характеристики на свою користь, тому час вставки та видалення займає максимум O(log n)
, що дуже швидко. Деякі з цих контейнерів map
, multimap
, set
, і multiset
.
Приклад коду дерева AVL можна знайти на веб- сайті http://ideone.com/MheW8
Основний додаток - двійкові дерева пошуку . Це структура даних, в якій пошук, вставлення та видалення все дуже швидко (про log(n)
операції)
Один цікавий приклад бінарного дерева, про яке не згадували, - це рекурсивно оцінений математичний вираз. Це практично марно з практичної точки зору, але це цікавий спосіб думати про такі вирази.
В основному кожен вузол дерева має значення, яке або притаманне саме йому, або оцінюється рекурсивно, оперуючи значеннями його дітей.
Наприклад, вираз (1+3)*2
можна виразити як:
*
/ \
+ 2
/ \
1 3
Для оцінки виразу ми запитуємо значення батьківського значення. Цей вузол, у свою чергу, отримує свої значення від своїх дітей, оператора плюс та вузла, який просто містить «2». Оператор плюс, у свою чергу, отримує свої значення від дітей зі значеннями '1' та '3' і додає їх, повертаючи 4 у вузол множення, який повертає 8.
Таке використання бінарного дерева подібне до зворотного позначення польської нотації в певному сенсі, оскільки порядок виконання операцій однаковий. Також слід зауважити, що це не обов'язково має бути двійковим деревом, це просто те, що найбільш часто використовувані оператори є двійковими. На самому базовому рівні, бінарне дерево тут є насправді дуже простою чисто функціональною мовою програмування.
Я не думаю, що для "чистих" двійкових дерев немає користі. (крім освітніх цілей) Збалансовані двійкові дерева, такі як червоно-чорні дерева або дерева AVL, є набагато кориснішими, оскільки вони гарантують операції O (logn). Звичайні бінарні дерева можуть бути списком (або майже списком) і не дуже корисні для додатків, що використовують багато даних.
Збалансовані дерева часто використовуються для реалізації карт або наборів. Вони також можуть бути використані для сортування в O (nlogn), навіть там існують кращі способи зробити це.
Також можна шукати / вставляти / видаляти таблиці Hash , які зазвичай мають кращу продуктивність, ніж двійкові дерева пошуку (врівноважені чи ні).
Застосування, де (врівноважене) двійкові дерева пошуку було б корисним, якщо знадобиться пошук / вставка / видалення та сортування. Сортування може бути на місці (майже, ігноруючи простір стеку, необхідний для рекурсії), з огляду на готове дерево для збалансованого побудови. Це все ще буде O (nlogn), але з меншим постійним коефіцієнтом і не потребує додаткового простору (за винятком нового масиву, якщо припустити, що дані потрібно помістити в масив). Таблиці хешу з іншого боку не можна сортувати (принаймні, не безпосередньо).
Можливо, вони також корисні в деяких складних алгоритмах для того, щоб щось робити, але нічого не спадає на думку. Якщо я знайду більше, я відредагую своє повідомлення.
Інші дерева, такі як дерева fe B + , широко використовуються в базах даних
Одне з найпоширеніших додатків - це ефективно зберігати дані у відсортованому вигляді для швидкого доступу та пошуку збережених елементів. Наприклад, std::map
або std::set
в стандартній бібліотеці C ++.
Бінарне дерево як структура даних є корисним для різних реалізацій аналізаторів вираження та вирішувачів виразів.
Він також може бути використаний для вирішення деяких проблем із базами даних, наприклад, індексації.
Як правило, бінарне дерево - це загальне поняття конкретної структури даних на основі дерева, і різні специфічні типи бінарних дерев можуть бути побудовані з різними властивостями.
У C ++ STL та багатьох інших стандартних бібліотеках іншими мовами, як-от Java та C #. Двійкові дерева пошуку використовуються для реалізації набору та карти.
Одне з найважливіших застосувань бінарних дерев - це збалансовані дерева бінарного пошуку, такі як:
Ці типи дерев мають властивість, що різниця у висоті лівого піддерева та правого піддерева підтримується невеликою, виконуючи такі операції, як обертання кожного разу, коли вузол вставляється чи видаляється.
Завдяки цьому загальна висота деревних залишків порядку журналу n та такі операції, як пошук, вставлення та видалення вузлів, виконуються в O (log n) час. STL з C ++ також реалізує ці дерева у вигляді наборів і карт.
На сучасному обладнанні бінарне дерево майже завжди є неоптимальним через погану поведінку кешу та простору. Це стосується і (напів) збалансованих варіантів. Якщо ви їх знайдете, це там, де продуктивність не враховується (або переважає функція порівняння), або, швидше за все, з історичних чи неосвічених причин.
Компілятор, який використовує двійкове дерево для представлення AST, може використовувати відомі алгоритми для розбору дерева, як postorder, inorder. Програмісту не потрібно придумувати власний алгоритм. Оскільки бінарне дерево для вихідного файлу вище, ніж n-ary дерево, його створення потребує більше часу. Візьміть це виробництво: selstmnt: = "if" "(" expr ")" stmnt "ELSE" stmnt У двійковому дереві він буде мати 3 рівні вузлів, але дерево n-ary матиме 1 рівень (з діток)
Ось чому ОС на базі Unix повільні.