Какое двоичное дерево вы бы порекомендовали?

18

Я изучаю Haskell и в качестве упражнения создаю бинарные деревья. Сделав обычное двоичное дерево, я хочу адаптировать его к самобалансирующемуся. Так:

  • Какой самый эффективный?
  • Что проще всего реализовать?
  • Что чаще всего используется?

Но главное, что вы рекомендуете?

Я предполагаю, что это принадлежит здесь, потому что это открыто для обсуждения.

dan_waterworth
источник
С точки зрения эффективности и простоты реализации, общая эффективность четко определена, но я считаю, что для вашей реализации лучше всего было бы реализовать столько, сколько вы можете найти, а затем сообщить нам, что работает лучше ...
glenatron

Ответы:

15

Я бы порекомендовал вам начать либо с красно-черного дерева , либо с дерева AVL .

Красно-черное дерево быстрее вставляется, но дерево AVL имеет небольшое преимущество при поиске. Дерево AVL, вероятно, немного проще в реализации, но не настолько сильно, исходя из моего собственного опыта.

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

Быстрый Джо Смит
источник
1
Лично я нахожу красно-черную вставку легче, чем вставку AVL. Причина кроется в (несовершенной) аналогии с B-деревьями. Вставки - это неудобно, но удаления - это зло (так много случаев нужно учитывать). На самом деле у меня больше нет собственной реализации удаления C ++ красно-черного цвета - я удалил ее, когда понял, что (1) я никогда не использовал ее - каждый раз, когда я хотел удалить, я удалял несколько элементов, поэтому я преобразовал из дерева в list, удалите из списка, затем преобразуйте обратно в дерево, и (2) оно все равно было сломано.
Steve314
2
@ Steve314, красно-черные деревья проще, но вы не смогли сделать реализацию, которая работает? Каковы деревья AVL тогда?
dan_waterworth
@dan_waterworth - я еще не реализовал даже метод вставки, который работает - у меня есть заметки, я понимаю основной принцип, но никогда не получал правильную комбинацию мотивации, времени и уверенности. Если бы я просто хотел, чтобы версии работали, это просто copy-pseudocode-from-textbook-and-translate (и не забывайте, что в C ++ есть стандартные контейнеры для библиотек), но где в этом удовольствие?
Steve314
Кстати, я полагаю (но не могу предоставить ссылку), что довольно популярный учебник содержит ошибочную реализацию одного из алгоритмов сбалансированного двоичного дерева - не уверен, но это может быть красно-черное удаление. Так что это не только я ;-)
Steve314
1
@ Steve314, я знаю, деревья могут быть очень сложными на императивном языке, но, что удивительно, их реализация в Haskell была легкой задачей. За выходные я написал обычное дерево AVL, а также одномерный пространственный вариант, и они всего около 60 строк.
dan_waterworth
10

Я бы рассмотрел альтернативу, если вы в порядке со случайными структурами данных: Пропускать списки .

С точки зрения высокого уровня, это древовидная структура, за исключением того, что она не реализована в виде дерева, а представляет собой список с несколькими слоями ссылок.

Вы получите O (log N) вставок / поисков / удалений, и вам не придется иметь дело со всеми этими сложными случаями ребалансировки.

Я никогда не думал о том, чтобы реализовать их на функциональном языке, и на странице википедии их нет, поэтому это может быть непросто (по сравнению с неизменностью)

Матье М.
источник
Мне действительно нравится пропускать списки, и я реализовал их раньше, но не на функциональном языке. Я думаю, что попробую их после этого, но сейчас я на самоуравновешивающихся деревьях.
dan_waterworth
Кроме того, люди часто используют пропуски для одновременных структур данных. Может быть, лучше, чем принудительная неизменность, использовать примитивы параллелизма haskell (такие как MVar или TVar). Хотя это не научит меня многому в написании функционального кода.
dan_waterworth
2
@ Fanatic23, список пропусков не является ADT. ADT является либо множеством, либо ассоциативным массивом.
dan_waterworth
@dan_waterworth Боже мой, ты прав.
Fanatic23
5

Если вы хотите, чтобы структура начала была относительно простой (как AVL-деревья, так и красно-чёрные деревья довольно неудобны), один из вариантов - это treap, который называется комбинацией «tree» и «heap».

Каждый узел получает значение «приоритет», часто назначаемое случайным образом при создании узла. Узлы расположены в дереве так, чтобы соблюдались упорядочивание ключей и соблюдались упорядочения значений приоритетов в виде кучи. Упорядочение в виде кучи означает, что оба потомка родителя имеют более низкий приоритет, чем родитель.

EDIT удален «в пределах значений ключа» выше - приоритет и порядок ключей применяются вместе, поэтому приоритет имеет значение даже для уникальных ключей.

Это интересная комбинация. Если ключи уникальны, а приоритеты уникальны, существует уникальная древовидная структура для любого набора узлов. Несмотря на это, вставки и удаления эффективны. Строго говоря, дерево может быть разбалансировано до такой степени, что оно фактически является связанным списком, но это крайне маловероятно (как со стандартными двоичными деревьями), в том числе для обычных случаев, таких как ключи, вставленные по порядку (в отличие от стандартных двоичных деревьев).

Steve314
источник
1
+1. Treaps - мой личный выбор, я даже написал сообщение в блоге о том, как они реализованы.
П Швед
5

Какой самый эффективный?

Расплывчато и сложно ответить. Вычислительные сложности все четко определены. Если это то, что вы подразумеваете под эффективностью, то никаких реальных дебатов нет. Действительно, все хорошие алгоритмы идут с доказательствами и факторами сложности.

Если вы имеете в виду «время выполнения» или «использование памяти», то вам нужно сравнить фактические реализации. Затем вступают в игру язык, время выполнения, ОС и другие факторы, что затрудняет ответ на вопрос.

Что проще всего реализовать?

Расплывчато и сложно ответить. Некоторые алгоритмы могут показаться сложными для вас, но тривиальными для меня.

Что чаще всего используется?

Расплывчато и сложно ответить. Сначала есть "кем?" часть этого? Только на Хаскеле? А как насчет C или C ++? Во-вторых, существует проблема проприетарного программного обеспечения, когда у нас нет доступа к источнику для проведения опроса.

Но главное, что вы рекомендуете?

Я предполагаю, что это принадлежит здесь, потому что это открыто для обсуждения.

Верный. Поскольку другие ваши критерии не очень полезны, это все, что вы собираетесь получить.

Вы можете получить исходники для большого количества древовидных алгоритмов. Если вы хотите чему-то научиться, вы можете просто реализовать все, что можете найти. Вместо того, чтобы просить «рекомендации», просто соберите все алгоритмы, которые вы можете найти.

Вот список:

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

Есть шесть популярных из них определены. Начните с тех.

С. Лотт
источник
3

Если вас интересуют Splay-деревья, есть более простая версия, которая, как мне кажется, была впервые описана в статье Аллена и Манро. Он не имеет таких же гарантий производительности, но позволяет избежать сложностей при работе с «зигзагом» и «зигзагом».

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

Как и в случае Splay-деревьев, идея заключается в том, что объекты, к которым недавно обращались, всегда находятся рядом с корнем дерева, поэтому быстрый доступ к ним снова. Проще говоря, эти деревья поворота к корню Allen-Munroe (как я их называю - не знаю официального названия) могут быть быстрее, но они не имеют такой же амортизированной гарантии производительности.

Одна вещь - поскольку эта структура данных по определению мутирует даже для операций поиска, ее, вероятно, нужно будет реализовать монадически. Я думаю, что это не очень подходит для функционального программирования.

Steve314
источник
Splays немного раздражает, учитывая, что они изменяют дерево даже при поиске. Это было бы довольно болезненно в многопоточных средах, что является одним из основных мотивов для использования функционального языка, такого как Haskell. Опять же, я никогда раньше не использовал функциональные языки, так что, возможно, это не фактор.
Быстрый Джо Смит
@Quick - зависит от того, как вы собираетесь использовать дерево. Если бы вы использовали его в истинном коде функционального стиля, вы бы либо отбрасывали мутацию при каждой находке (делая дерево Splay немного глупым), либо в конечном итоге дублировали бы значительную часть двоичного дерева при каждом поиске, и отслеживайте, с каким состоянием дерева вы работаете, когда ваша работа прогрессирует (причина, вероятно, использования монадического стиля). Это копирование может быть оптимизировано компилятором, если вы больше не ссылаетесь на старое древовидное состояние после создания нового (подобные предположения распространены в функциональном программировании), но это не так.
Steve314
Ни один из подходов не звучит оправданно. Опять же, ни один из них не делает чисто функциональные языки по большей части.
Быстрый Джо Смит
1
@Quick - Дублирование дерева - это то, что вы будете делать для любой древовидной структуры данных на чистом функциональном языке для мутирующих алгоритмов, таких как вставки. С точки зрения исходного кода, код не будет так сильно отличаться от императивного кода, который выполняет обновления на месте. Различия уже обработаны, предположительно, для несбалансированных бинарных деревьев. До тех пор, пока вы не попытаетесь добавить родительские ссылки на узлы, дубликаты будут иметь общие общие поддеревья как минимум, и глубокая оптимизация в Haskell является довольно жесткой, если не идеальной. Я сам против Хаскелла в принципе, но это не обязательно проблема.
Steve314
2

Очень простое сбалансированное дерево - это дерево АА . Его инвариант проще и, следовательно, легче реализовать. Из-за своей простоты его производительность все еще хороша.

В качестве расширенного упражнения вы можете попытаться использовать GADT для реализации одного из вариантов сбалансированных деревьев, инвариант которых обеспечивается типом системы типов.

Петр Пудлак
источник