В информатике бинарные деревья со сбалансированным весом ( WBT ) — это тип самобалансирующихся двоичных деревьев поиска , которые можно использовать для реализации динамических наборов , словарей (карт) и последовательностей. [1] Эти деревья были представлены Нивергельтом и Рейнгольдом в 1970-х годах как деревья ограниченного баланса , или BB[α]-деревья . [2] [3] Их более распространенное название связано с Кнутом . [4]
Хорошо известным примером является кодирование корпуса по Хаффману .
Как и другие самобалансирующиеся деревья, WBT хранят бухгалтерскую информацию, относящуюся к балансу, в своих узлах и выполняют вращения для восстановления баланса, когда он нарушается операциями вставки или удаления. В частности, каждый узел хранит размер поддерева, корнем которого является узел, а размеры левого и правого поддеревьев сохраняются в пределах некоторого различия друг от друга. В отличие от информации о балансе в деревьях AVL (использующих информацию о высоте поддеревьев) и красно-черных деревьях (которые хранят вымышленный бит «цвета»), информация о балансе в WBT является действительно полезным свойством для приложений: количество элементов в дереве равна размеру его корня, а информация о размере — это именно та информация, которая необходима для реализации операций дерева статистики порядка , а именно: получения n -го по величине элемента в наборе или определения индекса элемента в отсортированный порядок. [5]
Деревья со сбалансированным весом популярны в сообществе функционального программирования и используются для реализации множеств и карт в MIT Scheme , SLIB , SML-NJ и реализациях Haskell . [6] [4]
Дерево со сбалансированным весом — это двоичное дерево поиска, в узлах которого хранятся размеры поддеревьев. То есть узел имеет поля
По определению размер листа (обычно представленного нулевым указателем) равен нулю. Размер внутреннего узла — это сумма размеров двух его дочерних узлов плюс один: ( size[n] = size[n.left] + size[n.right] + 1 ). В зависимости от размера определяется вес: Weight[n] = size[n] + 1 . [a] Преимущество веса состоит в том, что вес узла представляет собой просто сумму весов его левого и правого дочерних элементов.
Операции, которые изменяют дерево, должны гарантировать, что вес левого и правого поддеревьев каждого узла остается в пределах некоторого коэффициента α друг от друга, используя те же операции ребалансировки, которые используются в деревьях AVL : повороты и двойные повороты. Формально баланс узла определяется следующим образом:
Здесь α — числовой параметр, который необходимо определить при реализации деревьев со сбалансированным весом. Большие значения α создают «более сбалансированные» деревья, но не все значения α подходят; Нивергельт и Рейнгольд доказали, что
является необходимым условием работы алгоритма балансировки. Более поздняя работа показала нижнюю границу 2/11 для α , хотя ее можно сделать сколь угодно маленькой , если использовать специальный (и более сложный) алгоритм ребалансировки. [7]
Правильное применение балансировки гарантирует, что дерево из n элементов будет иметь высоту [7]
Если α задано максимально допустимое значение, высота дерева со сбалансированным весом в наихудшем случае такая же, как высота красно-черного дерева при .
Количество операций балансировки, требуемых в последовательности из n вставок и удалений, линейно по n , т. е. балансировка требует постоянного объема накладных расходов в амортизированном смысле. [8]
Хотя поддержание дерева с минимальной стоимостью поиска требует четырех видов двойных вращений (LL, LR, RL, RR, как в дереве AVL) в операциях вставки/удаления, если нам нужна только логарифмическая производительность, LR и RL являются единственными необходимыми вращениями. за один проход сверху вниз. [9]
Для деревьев со сбалансированным весом было определено несколько операций над множествами: объединение , пересечение и разность множеств . Затем на основе этих функций набора можно реализовать быстрые массовые операции по вставке или удалению. Эти операции над множествами основаны на двух вспомогательных операциях: Split и Join . Благодаря новым операциям реализация деревьев со сбалансированным весом может стать более эффективной и поддающейся параллелизации. [10] [11]
Алгоритм соединения следующий:
функция joinRightWB(TL , k, T R ) (l, k', c) = Exposure(T L ) , если баланс(|T L |, |T R |) return Node(T L , k, T R ) else T' = joinRightWB(c, k, T R) ) (l', k', r') = выставлять(T') if (balance(|l|,|T'|)) return Node(l, k', T') else if (balance(|l|,|l'|) и Balance(|l|+|l'| ,|r'|)) return RotateLeft(Node(l, k', T')) else return RotateLeft(Node(l, k', RotateRight(T')) function joinLeftWB(T L , k, T R ) /* симметрично joinRightWB */функция join( TL , k, T R ) if (heavy(TL , TR ) ) return joinRightWB(TL , k, TR ) if (heavy( TR , T L )) return joinLeftWB( TL , к, Т Р ) Узел(TL , k, TR )
Здесь баланс означает две гири , и они сбалансированы. Exposure(v)=(l, k, r) означает извлечение левого дочернего элемента узла дерева , ключа узла и правого дочернего элемента . Node(l, k, r) означает создание узла из левого дочернего элемента , ключа и правого дочернего элемента .
Алгоритм разделения следующий:
функция Split(T, k) , если (T = ноль) возвращает (ноль, ложь, ноль) (L, (m, c), R) = выставлять (T) если (k = m) вернуть (L, true, R) , если (k < m) (L', b, R') = расщепление(L, k) вернуть (L', b, join(R', m, R)) если (k > m) (L', b, R') = расщепление(R, k) return (join(L, m, L'), b, R))
Объединение двух сбалансированных по весу деревьев t 1 и t 2 , представляющих множества A и B , представляет собой дерево t со сбалансированным по весу , которое представляет A ∪ B . Следующая рекурсивная функция вычисляет это объединение:
function Union(t 1 , t 2 ): если t 1 = nil: вернуть t 2 если t 2 = nil: вернуть t 1 t < , t > ← разделить t 2 на t 1 .root return join(union(left(t) 1 ), t < ), t 1 .root, Union(right(t 1 ), t > ))
Здесь предполагается, что Split возвращает два дерева: одно содержит ключи, меньшие, чем входной ключ, а другое — более крупные ключи. (Алгоритм неразрушающий , но существует и деструктивная версия.)
Алгоритм пересечения или различия аналогичен, но требует вспомогательной процедуры Join2 , такой же, как и Join , но без среднего ключа. Благодаря новым функциям объединения, пересечения или разности в дерево со сбалансированным весом можно вставлять или удалять один или несколько ключей. Поскольку Split и Union вызывают Join , но не имеют дело непосредственно с критериями балансировки деревьев со сбалансированным весом, такую реализацию обычно называют алгоритмами на основе соединения .
Сложность каждого объединения, пересечения и разности соответствует двум сбалансированным по весу деревьям размеров и . Эта сложность оптимальна по количеству сравнений. Что еще более важно, поскольку рекурсивные вызовы объединения, пересечения или различия независимы друг от друга, они могут выполняться параллельно с параллельной глубиной . [10] Когда , реализация на основе соединения имеет тот же вычислительно ориентированный ациклический граф (DAG), что и одноэлементная вставка и удаление, если корень большего дерева используется для разделения меньшего дерева.