Ответы:
Спорить о производительности бинарных деревьев не имеет смысла - это не структура данных, а семейство структур данных с разными характеристиками производительности. Хотя верно то, что несбалансированные двоичные деревья работают намного хуже, чем самобалансирующиеся двоичные деревья для поиска, существует много двоичных деревьев (таких как двоичные попытки), для которых «балансировка» не имеет смысла.
map
и set
объектах в библиотеках многих Языков.Причина, по которой двоичные деревья используются для поиска чаще, чем n-арные, состоит в том, что n-арные деревья более сложны, но обычно не дают реального преимущества в скорости.
В (сбалансированном) бинарном дереве с m
узлами для перехода от одного уровня к следующему требуется одно сравнение, и есть log_2(m)
уровни для общего количества log_2(m)
сравнений.
Напротив, n-арное дерево потребует log_2(n)
сравнения (используя бинарный поиск), чтобы перейти на следующий уровень. Поскольку существует log_n(m)
общее количество уровней, для поиска потребуется log_2(n)*log_n(m)
= log_2(m)
общее количество сравнений. Таким образом, хотя n-арные деревья более сложны, они не дают никаких преимуществ с точки зрения необходимости полного сравнения.
(Тем не менее, n-арные деревья по-прежнему полезны в нишевых ситуациях. Примерами, которые сразу приходят на ум, являются квад-деревья и другие деревья с разделением пространства, где разделение пространства с использованием только двух узлов на уровень сделает логику излишне сложной; 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)
over на 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), даже если существуют лучшие способы сделать это.
Также для поиска / вставки / удаления могут быть использованы хеш-таблицы , которые обычно имеют лучшую производительность, чем двоичные деревья поиска (сбалансированные или нет).
Приложение, где (сбалансированные) бинарные деревья поиска были бы полезны, было бы необходимо, если бы был необходим поиск / вставка / удаление и сортировка. Сортировка может быть на месте (почти без учета стекового пространства, необходимого для рекурсии), учитывая сбалансированное дерево готовой сборки. Это все равно будет O (nlogn), но с меньшим постоянным коэффициентом и без необходимости в дополнительном пространстве (за исключением нового массива, при условии, что данные должны быть помещены в массив). Хеш-таблицы с другой стороны не могут быть отсортированы (по крайней мере, не напрямую).
Может быть, они также полезны в некоторых изощренных алгоритмах для чего-то, но мне ничего не приходит в голову. Если я найду больше, я отредактирую свой пост.
Другие деревья, такие как fe B + , широко используются в базах данных.
Одним из наиболее распространенных приложений является эффективное хранение данных в отсортированном виде для быстрого доступа к хранимым элементам и их поиска. Например, std::map
или std::set
в C ++ Standard Library.
Бинарное дерево как структура данных полезно для различных реализаций синтаксических анализаторов и решателей выражений.
Он также может быть использован для решения некоторых проблем с базой данных, например, индексации.
Обычно двоичное дерево представляет собой общую концепцию конкретной структуры данных на основе дерева, и различные конкретные типы двоичных деревьев могут быть созданы с различными свойствами.
В C ++ STL и многих других стандартных библиотеках на других языках, таких как Java и C #. Двоичные деревья поиска используются для реализации множества и отображения.
Одним из наиболее важных приложений бинарных деревьев являются сбалансированные бинарные деревья поиска, такие как:
Эти типы деревьев обладают свойством того, что разница высот левого поддерева и правого поддерева поддерживается небольшой за счет выполнения операций, таких как повороты, каждый раз, когда узел вставляется или удаляется.
В связи с этим общая высота дерева остается порядка log n, а такие операции, как поиск, вставка и удаление узлов, выполняются за O (log n). STL C ++ также реализует эти деревья в виде наборов и отображений.
На современном оборудовании двоичное дерево почти всегда неоптимально из-за плохого поведения кэша и пространства. Это также относится к (полу) сбалансированным вариантам. Если вы их найдете, это где производительность не учитывается (или преобладает функция сравнения), или, скорее всего, по историческим причинам или по незнанию.
Компилятор, который использует двоичное дерево для представления AST, может использовать известные алгоритмы синтаксического анализа дерева, такие как postorder, inorder. Программисту не нужно придумывать свой собственный алгоритм. Поскольку двоичное дерево для исходного файла выше, чем n-арное дерево, его сборка занимает больше времени. Возьмем такой пример: selstmnt: = "if" "(" expr ")" stmnt "ELSE" stmnt В двоичном дереве будет 3 уровня узлов, а у n-арного 1 уровень (chids)
Вот почему ОС Unix работают медленно.