Чому червоно-чорні дерева такі популярні?


46

Здається, що скрізь, де я дивлюся, структури даних реалізуються за допомогою червоно-чорних дерев ( std::setу C ++, SortedDictionaryу C # тощо)

Щойно накривши (a, b), червоно-чорні та AVL дерева в моєму класі алгоритмів, ось що у мене вийшло (також розпитуючи професорів, переглядаючи кілька книжок і трохи гуглившись):

  • Дерева AVL мають меншу середню глибину, ніж червоно-чорні дерева, і тому пошук значення в дереві AVL стабільно швидший.
  • Червоно-чорні дерева вносять менш структурні зміни, щоб збалансувати себе, ніж дерева AVL, що може зробити їх потенційно швидшими для вставлення / видалення. Я кажу, що це може залежати від витрат на структурну зміну дерева, оскільки це буде багато залежати від часу виконання та впровадження (можливо, функціональна мова також буде абсолютно різною, коли дерево непорушне?)

В Інтернеті є багато орієнтирів, які порівнюють AVL та Червоно-чорні дерева, але мене вразило те, що мій професор в основному сказав, що зазвичай ти робиш одну з двох речей:

  • Або ви не дуже переймаєтесь ефективністю, і в цьому випадку 10-20% різниці AVL проти червоно-чорних у більшості випадків взагалі не матимуть значення.
  • Або ви дійсно дбаєте про продуктивність, і в такому випадку ви б вирвали і AVL, і червоно-чорні дерева, і поїхали з B-деревами, які можна налаштувати, щоб вони працювали набагато краще (або (a, b) -речі, я ' я покладу всіх в один кошик.)

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

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

Якщо говорити, чи є конкретна причина, чому червоно-чорні дерева використовуються повсюдно, коли, виходячи з сказаного вище, B-дерева повинні перевершувати їх? (як єдиний орієнтир, який я міг знайти, також показано http://lh3lh3.users.sourceforge.net/udb.shtml , але це може бути лише питанням конкретної реалізації). Або причина, чому всі використовують червоно-чорні дерева, тому що їх досить легко здійснити, або, кажучи іншими словами, важко погано реалізувати?

Крім того, як це змінюється, коли людина переходить до сфери функціональних мов? Схоже, що і Clojure, і Scala використовують масиви Hash, відображені на карті , де Clojure використовує коефіцієнт розгалуження 32.


8
Щоб додати свій біль, більшість статей, які порівнюють різні види пошукових дерев, виконують ... менше, ніж ідеальні експерименти.
Рафаель

1
Я сам ніколи цього не розумів, на мою думку, дерева AVL легше впроваджувати, ніж червоно-чорні дерева (менше випадків при переврівноваженні), і я ніколи не помічав суттєвої різниці в продуктивності.
Джорді Вермеулен

3
Відповідна дискусія наших друзів у stackoverflow Чому std :: map реалізований як червоно-чорне дерево? .
Гендрік Ян

Відповіді:


10

Процитуйте відповідь на запитання « Подорожі з кореня у AVL-деревах та Червоних чорних деревах »

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

Отже, вставка дерева RedBlack може бути реалізована без рекурсії, в деяких процесорах рекурсія дуже дорога, якщо ви перевиконали кеш функціональних викликів (наприклад, SPARC через використання вікна Реєстрація )

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

Також не ризикувати вичерпання стека - це користь.


Але збалансоване дерево з 2 ^ 32 вузлами вимагало б не більше ніж 32 рівні рекурсії. Навіть якщо кадр стека становить 64 байти, це не більше 2 кб простору стека. Чи може це насправді змінити? Я б сумнівався в цьому.
Бьорн Ліндквіст

@ BjörnLindqvist, На процесорі SPARC у 1990-х я часто отримував більше 10-ти кратної швидкості, змінюючи загальний шлях коду з глибини стека від 7 до 6! Прочитайте, як він реєстрував файли ....
Іан Рінгроуз

9

Я нещодавно досліджував цю тему, тож ось мої висновки, але майте на увазі, що я не є експертом у структурі даних!

Є деякі випадки, коли ви взагалі не можете використовувати B-дерева.

Один видатний випадок - std::mapвід C ++ STL. Стандарт вимагає, щоб insertвін не визнавав недійсним існуючі ітератори

Жодні ітератори чи посилання недійсні.

http://en.cppreference.com/w/cpp/container/map/insert

Це виключає B-дерево як реалізацію, оскільки вставка зміститься навколо існуючих елементів.

Ще один подібний випадок використання - нав'язливі структури даних. Тобто, замість того, щоб зберігати свої дані всередині вузла дерева, ви зберігаєте вказівники дітям / батькам всередині вашої структури:

// non intrusive
struct Node<T> {
    T value;
    Node<T> *left;
    Node<T> *right;
};
using WalrusList = Node<Walrus>;

// intrusive
struct Walrus {
    // Tree part
    Walrus *left;
    Walrus *right;

    // Object part
    int age;
    Food[4] stomach;
};

Ви просто не можете зробити нав'язливе B-дерево, оскільки воно не є лише структурою даних.

Нав'язливі червоно-чорні дерева використовуються, наприклад, в jemalloc для управління вільними блоками пам'яті. Це також популярна структура даних в ядрі Linux.

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

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

Існує ряд ароматів реалізації червоно-чорного дерева. Відомим є залишені нахилені червоними чорними деревами Роберт Седжевік ( ОБЕРЕЖНО! Є й інші варіанти, які також названі "ліва схиляючись", але використовують інший алгоритм). Цей варіант дійсно дозволяє виконувати обертання на шляху вниз по дереву, але йому не вистачає важливої ​​властивості амортизованої кількості кількості виправлень, і це робить його повільніше ( як вимірює автор jemalloc ). Або, як це висловлюєтьсяO(1)

Варіант Андерссона - червоно-чорні дерева, варіант Червоно-чорного дерева Sedgewick та дерева AVL - все простіший у застосуванні, ніж тут визначена структура RedBlackTree. На жаль, жодна з них не може гарантувати, що амортизований час, проведений на балансування, становить за оновлення.O(1)

Варіант, описаний в ондатаструктурах, використовує батьківські вказівники, рекурсивний пропуск вниз для вставки та ітераційний прохід циклу для виправлення. Рекурсивні дзвінки знаходяться в хвостових положеннях, і компілятори оптимізують це до циклу (я перевірив це в Rust).

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


3

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

1) Середня вартість вставки є постійною для червоно-чорних дерев (якщо вам не доведеться шукати), в той час як вона є логарифмічною для дерев AVL. Крім того, вона передбачає щонайменше одну складну реструктуризацію. У гіршому випадку це все-таки O (log N), але це просто прості переробки.

2) Для них потрібен лише 1 біт додаткової інформації на вузол, і ви часто можете знайти спосіб отримати це безкоштовно.

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

4) Те, як червоно-чорне дерево розбиває відповідний 2-4 деревний вузол і вставляє середню клавішу в батьківський 2-4 вузол лише шляхом перефарбовування, є надзвичайно елегантним. Я просто люблю це робити.


0

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

Я створив власну альтернативу в std::setрамках великого проекту з ряду причин продуктивності. Я вибрав AVL над червоно-чорним з міркувань продуктивності (але це невелике підвищення продуктивності не було виправданням для прокатки мого власного замість std :: set). Важливим фактором було «складність» ключа та його важке переміщення. Чи мають (а, б) дерева все-таки сенс, якщо вам потрібен інший рівень непрямості перед клавішами? AVL та червоно-чорні дерева можна реструктурувати без переміщення ключів, тому вони мають таку перевагу, коли ключі дорого переміщувати.


За іронією долі, червоно-чорні дерева є "лише" особливим випадком (a, b) -черевинок, тому справа, здається, зводиться до налаштування параметрів? (cc @Gilles)
Рафаель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.