Чи організовані дерева структурою "первістка, нежитлова"? Якщо ні, то чому б і ні?


12

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

       +-----------------------------------------+
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------------+    +---------------+    +---------------+
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

Це здається природним, але виникає з деякими проблемами. Наприклад, коли кількість дочірніх вузлів змінюється, вам потрібно щось на зразок масиву чи списку для управління дітьми.

Використовуючи натомість лише (першу) дочірню та наступну (вказівку) побратимів, ми отримуємо щось таке:

       +-------------------+
       |        root       |
       | child    sibling  +--->NULL
       +--+----------------+
          |             
+----------------+    +----------------+    +----------------+
|    node1       |    |     node2      |    |     node3      |
| child  sibling +--->| child  sibling +--->| child  sibling +--->NULL
+--+-------------+    +--+-------------+    +--+-------------+
   |                     |                     |

Очевидно, що така структура може також добре представляти дерева, але це також пропонує деякі переваги. Найголовніше, що нам більше не потрібно турбуватися про кількість дочірніх вузлів. Використовуючи для дерева розбору, воно пропонує природне зображення для такого терміна, як "a + b + c + d + e", не перетворюючись на глибоке дерево.

Чи пропонують бібліотеки колекцій такі структури дерев? Чи використовують аналізатори таку структуру? Якщо ні, то які причини?


2
Що ж, ця структура, очевидно, коштує більш високої складності. Це варто лише в тому випадку, якщо вам дійсно потрібна змінна кількість дітей. Багато дерев мають фіксовану кількість дітей (або хоча б фіксований максимум), притаманних їх дизайну. У цих випадках додаткові непрямі значення не додають ніякої цінності.
Йоахім Зауер

4
Якщо розмістити елементи у пов'язаному списку, вводиться O(n)фактор в алгоритм.

І щоб дістатися до node3 з root, вам потрібно взяти cddar root ...
Tacroy

Tacroy: Правильно, знайти назад до кореня не зовсім просто, але якщо мені це справді потрібно, зворотний вказівник був би доречним (хоча це зіпсує діаграму ;-)
user281377

Відповіді:


7

Дерева, як і списки, - це "абстрактні типи даних", які можна реалізувати різними способами. Кожен спосіб має свої переваги та недоліки.

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

У другому прикладі головна перевага полягає в тому, що ви завжди додаєте дитину в O (1). Основним недоліком є ​​те, що випадковий доступ до дитини коштує O (n). Крім того, це може бути менш цікавим для величезних дерев з двох причин: у нього є об'єм пам'яті на один заголовок об'єкта та два вказівники на вузол, а вузли випадковим чином розповсюджуються на пам’ять, що може спричинити багато перемикань між кешем процесора та пам'ять при переході до дерева, що робить цю реалізацію менш привабливою для них. Це не є проблемою для звичайних дерев та додатків.

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


1
Наприклад: дерево B + ніколи б не використовувало цю структуру "первісток, зв'язок". Це було б неефективно до абсурду для дерева, що базується на диску, і все ще дуже неефективно для дерева на основі пам'яті. R-дерево пам'яті могло би терпіти цю структуру, але це все-таки означало б набагато більше кеш-пропусків. Мені важко думати про ситуацію, коли «первістка, нестиглість» була б вищою. Ну так, це може працювати для синтаксичного дерева, як згадується ammoQ. Ще щось?
Qwertie

3
"Ви завжди додаєте дитину в O (1)" - Я думаю, ви завжди можете вставити дитину в індексі 0 в O (1), але додавання дитини, здається, явно O (n).
Скотт Вітлок

Зберігання всього дерева в одному масиві є загальним для купи.
Брайан

1
@Scott: добре, я припустив, що зв'язаний список також містить вказівник / посилання на останній елемент, який би зробив його O (1) для першого чи останнього позиції ... хоча він відсутній у прикладі ОП
dagnelies

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

2

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

Моя улюблена реалізація дерева (вузла) вашої першої моделі - це однолінійний (в C #):

public class node : List<node> { /* props go here */ }

Спадковуйте із загального списку вашого власного типу (або успадковуйте від будь-якого іншого загального колекціонування власного типу). Ходити можливо в одному напрямку: сформуйте корінь вниз (предмети не знають своїх батьків).

Дерево лише для батьків

Ще одна модель, яку ви не згадали, - це та, де кожна дитина має посилання на свого батька:

               null
                 |
       +---------+---------------------------------+
       |       parent                              |
       | root                                      |
       +-------------------------------------------+
          |                   |                |
+---------+------+    +-------+--------+    +--+-------------+
|     parent     |    |     parent     |    |     parent     |
|     node 1     |    |     node 2     |    |     node 3     |
+----------------+    +----------------+    +----------------+

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

Ці дерева лише для батьків є звичайними програмами баз даних. Досить легко знайти дітей вузла з виразами "SELECT * WHERE ParentId = x". Однак ми рідко зустрічаємо ці перетворені в об'єкти класу дерева дерева як такі. У державних (настільних) додатках вони можуть бути зафіксовані в існуючі елементи управління деревом. У веб-програмах без громадянства навіть це може бути малоймовірним. Я бачив ORM-картографічні інструменти-генератори класів, які викидають помилки переповнення стека під час генерації класів для таблиць, які мають відношення до себе (хихикання), тому, можливо, ці дерева не є такими поширеними.

двонаправлені судноплавні дерева

Однак у більшості практичних випадків зручно мати найкращі з обох світів. Вузли, у яких є список дітей і крім того, знають свого батька: двонаправлені навігаційні дерева.

                          null
                            |
       +--------------------+--------------------+
       |                  parent                 |
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------+-----+    +-------+-------+    +---+-----------+
|      parent   |    |     parent    |    |  parent       |
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

Це враховує ще багато аспектів, які слід враховувати:

  • Де здійснити зв'язування та від’єднання батьків?
    • нехай дбає про логіку ділової активності, а аспект залиште поза вузла (вони забудуть!)
    • у вузлах є методи створення дітей (не дозволяє повторно замовляти) (вибір Microsofts в їх реалізації System.Xml.XmlDocument DOM, який майже звів мене з розуму, коли я вперше зіткнувся з цим)
    • Вузли приймають батьків у своєму конструкторі (не дозволяє повторно замовляти)
    • у всіх методах додати (), вставити () та видалити () та їх перевантаження вузлів (зазвичай мій вибір)
  • Наполегливість
    • Як ходити по дереву при збереженні (залиште, наприклад, батьківські посилання)
    • Як відновити двостороння зв'язок після десеріалізації (встановлення всіх батьків знову як постдесеріалізаційна дія)
  • Сповіщення
    • Статичні механізми (прапор IsDirty), обробляють рекурсивно властивості?
    • Події, підказки через батьків, вниз через дітей або обома способами (розглянемо, наприклад, насос для повідомлень Windows).

Тепер, щоб відповісти на питання , двоспрямовані судноплавні дерева, як правило, (в моїй кар’єрі та на сьогоднішній день) є найбільш широко використовуваними. Прикладами є реалізація Microsofts системи System.Windows.Forms.Control або System.Web.UI.Control у рамках .Net, а також кожна реалізація DOM (Document Object Model) матиме вузли, які знають свого батьківського, а також перерахування своїх дітей. Причина: простота використання над простотою реалізації. Крім того, це зазвичай базові класи для більш конкретних класів (XmlNode може бути базою класів тегів, атрибутів і тексту), і ці базові класи є природними місцями для розміщення загальної архітектури серіалізації та обробки подій.

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


1

Я не знаю жодної бібліотеки контейнерів, яка безпосередньо підтримує ваш другий випадок, але більшість контейнерних бібліотек легко підтримують цей сценарій. Наприклад, в C ++ ви можете мати:

class Node;  // forward reference to satisfy the compiler
typedef std::list<Node*> NodeList;
class Node : public NodeList { /* . . . */ };  // a node is also a list

Node* n = new Node;
n->push_back(new Node);
Node* tree = new Node;
tree->push_back(new Node);
tree->push_back(n);

Парсери, ймовірно, використовують структуру, подібну до цієї, оскільки вона ефективно підтримує вузли зі змінною кількістю елементів та дітей. Я точно не знаю, тому що зазвичай не читаю їх вихідний код.


1

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

Інший приклад, коли наявність «покажчиків» для всіх дітей дозволяє зручніше використовувати. Наприклад, обидва описані вами типи можуть використовуватися при реалізації відносин дерева з реляційною базою даних. Але перший (в даному випадку детальний опис від батьків до дітей) дозволить запитувати корисні дані із загальним SQL, а останній обмежить вас.

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