Треп | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Тип | Рандомизированное двоичное дерево поиска | |||||||||||||||||||||||
|
Часть серии статей о |
Вероятностные структуры данных |
---|
Случайные деревья |
Связанный |
В информатике treap и рандомизированное бинарное дерево поиска — это две тесно связанные формы структур данных бинарного дерева поиска , которые поддерживают динамический набор упорядоченных ключей и позволяют выполнять бинарный поиск среди ключей. После любой последовательности вставок и удалений ключей форма дерева представляет собой случайную величину с тем же распределением вероятностей, что и случайное бинарное дерево; в частности, с высокой вероятностью его высота пропорциональна логарифму числа ключей, так что каждая операция поиска, вставки или удаления занимает логарифмическое время.
Дерево было впервые описано Раймундом Зайделем и Сесилией Р. Арагон в 1989 году; [1] [2] его название является гибридом слов tree и heap . Это декартово дерево , в котором каждому ключу присваивается (случайно выбранный) числовой приоритет. Как и в любом бинарном дереве поиска, порядок обхода узлов в симметричном порядке совпадает с порядком сортировки ключей. Структура дерева определяется требованием, чтобы оно было упорядочено в куче: то есть номер приоритета для любого нелистового узла должен быть больше или равен приоритету его дочерних узлов. Таким образом, как и в случае декартовых деревьев в более общем смысле, корневой узел является узлом с максимальным приоритетом, а его левое и правое поддеревья формируются таким же образом из подпоследовательностей отсортированного порядка слева и справа от этого узла.
Эквивалентный способ описания дерева заключается в том, что оно может быть сформировано путем вставки узлов с наивысшим приоритетом в двоичное дерево поиска без выполнения какой-либо перебалансировки. Следовательно, если приоритеты являются независимыми случайными числами (из распределения по достаточно большому пространству возможных приоритетов, чтобы гарантировать, что два узла вряд ли будут иметь одинаковый приоритет), то форма дерева имеет то же распределение вероятностей, что и форма случайного двоичного дерева поиска , дерева поиска, сформированного путем вставки узлов без перебалансировки в случайно выбранном порядке вставки. Поскольку известно, что случайные двоичные деревья поиска имеют логарифмическую высоту с высокой вероятностью, то же самое верно и для деревьев. Это отражает аргумент двоичного дерева поиска о том, что быстрая сортировка выполняется за ожидаемое время. Если двоичные деревья поиска являются решениями для версии динамической задачи сортировки, то деревья соответствуют именно динамической быстрой сортировке, где приоритеты определяют выбор опорных точек.
Арагон и Сайдель также предлагают назначать более высокие приоритеты часто используемым узлам, например, с помощью процесса, который при каждом доступе выбирает случайное число и заменяет приоритет узла этим числом, если он выше предыдущего приоритета. Эта модификация приведет к тому, что дерево потеряет свою случайную форму; вместо этого часто используемые узлы с большей вероятностью будут находиться вблизи корня дерева, что приведет к ускорению их поиска.
Наор и Ниссим [3] описывают приложение для поддержания сертификатов авторизации в криптосистемах с открытым ключом .
Treaps поддерживают следующие основные операции:
В дополнение к операциям вставки, удаления и поиска отдельных элементов, для деревьев определены несколько быстрых «объемных» операций: union , crossing и set difference . Они опираются на две вспомогательные операции: split и join .
Алгоритм объединения следующий:
функция join(L, k, R) если prior(k, k(L)) и prior(k, k(R)) возвращает Node(L, k, R) если prior(k(L), k(R)) возвращает Node(left(L), k(L), join(right(L), k, R)) возвращает Node(join(L, k, left(R)), k(R), right(R))
Алгоритм разделения следующий:
функция split(T, k) если (T = nil) возвращает (nil, false, nil) (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) возврат (join(L, m, L'), b, R'))
Объединение двух деревьев t 1 и t 2 , представляющих множества A и B , является деревом t , представляющим A ∪ B. Следующий рекурсивный алгоритм вычисляет объединение:
функция union(t 1 , t 2 ): если t 1 = nil: вернуть t 2 если t 2 = nil: вернуть t 1 если priority(t 1 ) < priority(t 2 ): поменять местами t 1 и t 2 t < , t > ← разделить t 2 по key(t 1 ) вернуть join(union(left(t 1 ), t < ), key(t 1 ), объединение(право(t 1 ), t > ))
Здесь предполагается , что split возвращает два дерева: одно, содержащее ключи, меньшие входного ключа, и одно, содержащее большие ключи. (Алгоритм является неразрушающим , но существует также и разрушающая версия на месте.)
Алгоритм для пересечения похож, но требует вспомогательной процедуры join . Сложность каждого из объединения, пересечения и разности составляет O ( m log н/м ) для деревьев размеров m и n , где m ≤ n . Более того, поскольку рекурсивные вызовы union независимы друг от друга, они могут выполняться параллельно . [4]
Разделение и объединение вызывают объединение, но не имеют дела с критериями балансировки деревьев напрямую, такая реализация обычно называется реализацией «на основе объединения» .
Обратите внимание, что если хэш-значения ключей используются в качестве приоритетов, а структурно равные узлы объединяются уже на этапе построения, то каждый объединенный узел будет уникальным представлением набора ключей. При условии, что может быть только один одновременный корневой узел, представляющий данный набор ключей, два набора могут быть проверены на равенство путем сравнения указателей, что является постоянным во времени.
Этот метод можно использовать для улучшения алгоритмов слияния, чтобы они работали быстро, даже когда разница между двумя наборами невелика. Если входные наборы равны, функции объединения и пересечения могут немедленно прерваться, вернув один из входных наборов в качестве результата, в то время как функция разности должна возвращать пустой набор.
Пусть d будет размером симметричной разности. Модифицированные алгоритмы слияния тогда также будут ограничены O ( d log н/г ) . [5] [6]
Рандомизированное двоичное дерево поиска, представленное Мартинесом и Роурой после работы Арагона и Зайделя по деревьям, [7] хранит те же узлы с тем же случайным распределением формы дерева, но поддерживает различную информацию в узлах дерева, чтобы сохранить его рандомизированную структуру.
Вместо того, чтобы хранить случайные приоритеты на каждом узле, рандомизированное двоичное дерево поиска хранит небольшое целое число на каждом узле, количество его потомков (считая себя за единицу); эти числа могут поддерживаться во время операций вращения дерева только за постоянное дополнительное количество времени на оборот. Когда ключ x должен быть вставлен в дерево, которое уже имеет n узлов, алгоритм вставки выбирает с вероятностью 1/( n + 1) поместить x в качестве нового корня дерева, а в противном случае он вызывает процедуру вставки рекурсивно, чтобы вставить x в левое или правое поддерево (в зависимости от того, меньше или больше его ключ корня). Количество потомков используется алгоритмом для вычисления необходимых вероятностей для случайного выбора на каждом шаге. Размещение x в корне поддерева может быть выполнено либо как в дереве, путем вставки его в лист и последующего вращения вверх, либо с помощью альтернативного алгоритма, описанного Мартинесом и Роурой, который разбивает поддерево на две части, которые будут использоваться в качестве левого и правого потомков нового узла.
Процедура удаления для рандомизированного бинарного дерева поиска использует ту же информацию на узел, что и процедура вставки, но в отличие от процедуры вставки, ей требуется в среднем только O(1) случайных решений для объединения двух поддеревьев, нисходящих от левого и правого потомков удаленного узла в одно дерево. Это связано с тем, что поддеревья, которые нужно объединить, в среднем находятся на глубине Θ(log n); объединение двух деревьев размером n и m требует в среднем Θ(log(n+m)) случайных выборов. Если левое или правое поддерево удаляемого узла пусто, операция объединения тривиальна; в противном случае левый или правый потомок удаленного узла выбирается в качестве нового корня поддерева с вероятностью, пропорциональной количеству его потомков, и объединение выполняется рекурсивно.
Информация, хранящаяся на каждом узле в рандомизированном двоичном дереве, проще, чем в дереве (небольшое целое число, а не случайное число высокой точности), но оно делает большее количество вызовов генератора случайных чисел (O(log n ) вызовов на вставку или удаление, а не один вызов на вставку), а процедура вставки немного сложнее из-за необходимости обновления количества потомков на узел. Небольшое техническое отличие состоит в том, что в дереве существует небольшая вероятность коллизии (два ключа получают одинаковый приоритет), и в обоих случаях будут статистические различия между истинным генератором случайных чисел и генератором псевдослучайных чисел, обычно используемым на цифровых компьютерах. Однако в любом случае различия между теоретической моделью идеального случайного выбора, используемой для разработки алгоритма, и возможностями реальных генераторов случайных чисел исчезающе малы.
Хотя дерево и рандомизированное бинарное дерево поиска имеют одинаковое случайное распределение форм деревьев после каждого обновления, история изменений деревьев, выполненных этими двумя структурами данных в ходе последовательности операций вставки и удаления, может различаться. Например, если в дереве три числа 1, 2 и 3 вставляются в порядке 1, 3, 2, а затем число 2 удаляется, оставшиеся два узла будут иметь те же родительско-дочерние отношения, что и до вставки среднего числа. В рандомизированном бинарном дереве поиска дерево после удаления с равной вероятностью будет любым из двух возможных деревьев на своих двух узлах, независимо от того, как выглядело дерево до вставки среднего числа.
Неявный древовидный массив [8] [ ненадежный источник? ] — это простая вариация обычного древовидного массива, которую можно рассматривать как динамический массив, поддерживающий следующие операции :
Идея неявного дерева заключается в использовании индекса массива в качестве ключа, но не в его явном хранении. В противном случае обновление (вставка/удаление) приведет к изменению ключей в узлах дерева.
Значение ключа ( неявный ключ) узла T — это количество узлов, меньших этого узла, плюс один. Обратите внимание, что такие узлы могут присутствовать не только в его левом поддереве, но и в левых поддеревьях его предков P, если T находится в правом поддереве P.
Поэтому мы можем быстро вычислить неявный ключ текущего узла, выполняя операцию, накапливая сумму всех узлов по мере спуска по дереву. Обратите внимание, что эта сумма не меняется, когда мы посещаем левое поддерево, но она увеличится на , когда мы посещаем правое поддерево.
Алгоритм соединения для неявного дерева выглядит следующим образом:
void join ( питем & т , питем л , питем р ) { если ( ! л || ! р ) т = л ? л : г ; иначе если ( l -> предыдущий > r -> предыдущий ) присоединиться ( l -> r , l -> r , r ), t = l ; еще присоединиться ( r -> l , l , r -> l ), t = r ; upd_cnt ( т ); }
[8] Алгоритм разделения для неявного дерева выглядит следующим образом:
void split ( pitem t , pitem & l , pitem & r , int key , int add = 0 ) { если ( ! т ) вернуть пустоту ( l = r = 0 ); int cur_key = add + cnt ( t -> l ); // неявный ключ если ( ключ <= cur_key ) разделить ( t -> l , l , t -> l , ключ , добавить ), r = t ; еще разделить ( t -> r , t -> r , r , key , add + 1 + cnt ( t -> l )), l = t ; upd_cnt ( т ); }
[8]
Чтобы вставить элемент в позицию pos, мы делим массив на две подсекции [0...pos-1] и [pos..sz], вызывая функцию split , и получаем два дерева и . Затем мы объединяем с новым узлом, вызывая функцию join . Наконец, мы вызываем функцию join для объединения и .
Мы находим элемент, который необходимо удалить, и выполняем соединение его дочерних элементов L и R. Затем мы заменяем элемент, который необходимо удалить, деревом, полученным в результате операции соединения.
Для выполнения этого расчета мы поступим следующим образом:
Для выполнения этой операции мы поступим следующим образом:
Чтобы показать, что поддерево данного узла необходимо перевернуть для каждого узла, мы создадим дополнительное логическое поле R и установим его значение в true. Чтобы распространить это изменение, мы поменяем местами дочерние элементы узла и установим R в true для всех них.
{{citation}}
: CS1 maint: DOI неактивен по состоянию на ноябрь 2024 г. ( ссылка ){{cite journal}}
: Цитировать журнал требует |journal=
( помощь )