stringtranslate.com

Распространение дерева

Расширенное дерево — это двоичное дерево поиска с дополнительным свойством, благодаря которому элементы, к которым недавно обращались, можно быстро получить снова. Подобно самобалансирующимся двоичным деревьям поиска , расширенное дерево выполняет основные операции, такие как вставка, поиск и удаление, за амортизированное время O (log n ) . Для шаблонов произвольного доступа, полученных из неравномерного случайного распределения, их амортизированное время может быть быстрее логарифмического, пропорционального энтропии шаблона доступа. Кроме того, для многих шаблонов неслучайных операций расширение деревьев может занять время, превышающее логарифмическое, без необходимости предварительного знания шаблона. Согласно недоказанной гипотезе динамической оптимальности, их производительность для всех шаблонов доступа находится в пределах постоянного коэффициента наилучшей возможной производительности, которая может быть достигнута любым другим самонастраивающимся двоичным деревом поиска, даже выбранным для соответствия этому шаблону. Развернутое дерево было изобретено Дэниелом Слитером и Робертом Тарджаном в 1985 году. [1]

Все обычные операции над двоичным деревом поиска объединены одной базовой операцией, называемой расширением . Расширение дерева для определенного элемента переупорядочивает дерево так, что элемент помещается в корень дерева. Один из способов сделать это с помощью базовой операции поиска — сначала выполнить стандартный поиск в двоичном дереве для рассматриваемого элемента, а затем использовать поворот дерева определенным образом, чтобы вывести элемент наверх. Альтернативно, нисходящий алгоритм может объединить поиск и реорганизацию дерева в один этап.

Преимущества

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

Преимущества включают в себя:

Недостатки

Наиболее существенным недостатком расширенных деревьев является то, что высота расширенного дерева может быть линейной. [2] : 1  Например, это будет иметь место после доступа ко всем n элементам в неубывающем порядке. Поскольку высота дерева соответствует наихудшему времени доступа, это означает, что фактическая стоимость одной операции может быть высокой. Однако амортизированная стоимость доступа в этом худшем случае является логарифмической, O(log n ). Кроме того, ожидаемую стоимость доступа можно уменьшить до O(log n ), используя рандомизированный вариант. [4]

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

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

Операции

Растягивание

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

Каждый конкретный шаг зависит от трех факторов:

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

Зигзагообразный шаг: этот шаг выполняется, когда p является корнем. Дерево поворачивается на ребре между x и p . Шаги Zig существуют для решения проблемы четности, они будут выполняться только как последний шаг в операции расширения и только тогда, когда x имеет нечетную глубину в начале операции.

Зигзагообразный шаг: этот шаг выполняется, когда p не является корнем, а x и p либо оба правые дочерние элементы, либо оба являются левыми дочерними элементами. На рисунке ниже показан случай, когда x и p являются левыми дочерними элементами. Дерево поворачивается на ребре, соединяющем p с его родительским элементом g , затем поворачивается на ребре, соединяющем x с p . Зигзагообразные шаги — единственное, что отличает расширенные деревья от метода вращения к корню, предложенного Алленом и Манро [5] до введения расширенных деревьев.

Зигзагообразный шаг: этот шаг выполняется, когда p не является корнем, x — правым дочерним элементом, а p — левым дочерним элементом, или наоборот ( x — левый, p — правый). Дерево поворачивается на ребре между p и x , а затем поворачивается на результирующем ребре между x и g .

Присоединиться

Учитывая два дерева S и T, в которых все элементы S меньше элементов T, можно использовать следующие шаги, чтобы объединить их в одно дерево:

Расколоть

Учитывая дерево и элемент x , верните два новых дерева: одно, содержащее все элементы, меньшие или равные x , а другое, содержащее все элементы, большие, чем x . Это можно сделать следующим образом:

Вставка

Чтобы вставить значение x в дерево отображения:

В результате вновь вставленный узел x становится корнем дерева.

Альтернативно:

Удаление

Чтобы удалить узел x , используйте тот же метод, что и для двоичного дерева поиска:

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

Альтернативно:

Реализация и варианты

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

Другой метод, который можно использовать, основан на том, что дерево можно реструктурировать на пути вниз по пути доступа вместо второго прохода. В этой процедуре расширения сверху вниз используются три набора узлов — левое дерево, правое дерево и среднее дерево. Первые два содержат все элементы исходного дерева, которые, как известно, меньше или больше текущего элемента соответственно. Среднее дерево состоит из поддерева, корнем которого является текущий узел. Эти три набора обновляются по пути доступа, сохраняя при этом операции расширения. Другой метод, полурасширение, модифицирует вариант зигзага, чтобы уменьшить объем реструктуризации, выполняемой во всех операциях. [1] [6]

Ниже представлена ​​реализация расширенных деревьев на C++, в которой для представления каждого узла дерева используются указатели. Эта реализация основана на версии расширения снизу вверх и использует второй метод удаления в дереве расширения. Кроме того, в отличие от приведенного выше определения, эта версия C++ не расширяет дерево при поиске — оно расширяется только при вставках и удалениях, и поэтому операция поиска имеет линейную временную сложность.

#include <функционал> #ifndef SPLAY_TREE #define SPLAY_TREEшаблон < имя_типа T , имя_типа Comp = std :: less < T >> class splay_tree { private : Comp comp ; беззнаковый длинный p_size ; struct node { node * влево , * вправо ; узел * родительский ; клавиша Т ; узел ( const T & init = T ()) : левый ( nullptr ), правый ( nullptr ), родительский ( nullptr ), ключ ( init ) { } ~ node () {                                      } } * корень ; void left_rotate ( узел * x ) { узел * y = x -> right ; если ( y ) { Икс -> вправо = y -> влево ; if ( y -> left ) y -> left -> родительский = x ; y -> родительский = x -> родительский ; } if ( ! x -> родительский ) root = y ; else if ( x == x -> родительский -> left ) x -> родительский -> left = y ; иначе x -> родительский -> right = y ; если ( y ) y -> влево = x ; х -> родитель = y ; } void right_rotate ( узел * x ) { node * y = x -> left ; если ( y ) { Икс -> влево = y -> вправо ; if ( y -> вправо ) y -> вправо -> родительский = x ; y -> родительский = x -> родительский ; } if ( ! x -> родительский ) root = y ; else if ( x == x -> родительский -> left ) x -> родительский -> left = y ; иначе х ->                                                                                            родитель -> справа = y ; если ( y ) y -> вправо = x ; х -> родитель = y ; } void splay ( node * x ) { while ( x -> родительский ) { if ( !x -> родительский -> родительский ) { if ( x -> родительский -> left == x ) right_rotate ( x -> родительский ); иначе left_rotate ( x -> родительский ); } else if ( x -> родительский -> left == x && x -> родительский -> родительский -> left == x -> родительский ) { right_rotate ( x -> родительский -> родительский ); right_rotate ( x -> родительский ); } else if ( x -> родительский -> правый == x && x -> родительский -> родительский -> правый == x -> родительский ) { left_rotate ( x -> родительский -> родительский ); left_rotate ( x -> родительский ); } else if ( x -> родительский -> left == x && x -> родительский -> родительский -> right == x -> родительский ) { right_rotate ( x -> родительский ); left_rotate ( x -> родительский ); } еще {                                                                        left_rotate ( x -> родительский ); right_rotate ( x -> родительский ); } } } void replace ( node * u , node * v ) { if ( ! u -> родительский ) root = v ; иначе if ( u == u -> родительский -> left ) u -> родительский -> left = v ; иначе u -> родительский -> right = v ; если ( v ) v -> родитель = u -> родитель ; } node * subtree_minimum ( node * u ) { while ( u -> left ) u = u -> left ; вернуть тебя ; } node * subtree_maximum ( node * u ) { while ( u -> right ) u = u -> right ; вернуть тебя ; } Public : splay_tree () : root ( nullptr ), p_size ( 0 ) { } void Insert ( const T & key ) { node * z = root ; узел * p = nullptr ; в то время как ( z ) { п знак равно z ; если ( comp ( z -> ключ , ключ )) z = z -> вправо ; иначе z = z ->                                                                                                 левый ; } Z = новый узел ( ключ ); г -> родитель = п ; если ( ! p ) корень = z ; иначе if ( comp ( p -> key , z -> key )) p -> right = z ; иначе р -> влево = z ; разворот ( z ); p_размер ++ ; } node * find ( const T & key ) { node * z = root ; while ( z ) { if ( comp ( z -> ключ , ключ )) z знак равно z -> вправо ; иначе if ( comp ( key , z -> key )) z = z -> left ; иначе вернуть z ; } Вернуть нульптр ; } void Erase ( const T & key ) { node * z = find ( key ); если ( ! z ) возврат ; разворот ( z ); если ( ! z -> влево ) заменить ( z , z -> вправо ); иначе , если ( ! z -> вправо ) заменить ( z , z -> влево ); еще { узел * y = subtree_minimum ( z -> вправо ); если ( у ->                                                                                                родитель != z ) { replace ( y , y -> вправо ); y -> вправо = z -> вправо ; y -> вправо -> родительский = y ; } Заменить ( z , y ); y -> влево = z -> влево ; y -> слева -> родительский = y ; } Удалить г ; p_размер -- ; }                          /* //альтернативная реализация  void Erase(const T &key) {  node *z = find(key);  если (!z) возврат;  расширение (г);  узел *s = z->влево;  узел *t = z-> вправо;  удалить z;  узел *sMax = NULL;  если (s) {  s->parent = NULL;  sMax = subtree_maximum(s);  расширение (sMax);  корень = сМакс;  }  Если (т) {  если (ы)  sMax->right = t;  еще  корень = т;  т->родитель = sMax;  }  p_size--;  } */ const T & минимум () { return subtree_minimum ( корень ) -> ключ ; } const T & Maximum () { return subtree_maximum ( root ) -> key ; } bool пустой () const { return root == nullptr ; } unsigned long size () const { return p_size ; } };                                     #endif // SPLAY_TREE

Анализ

Простой амортизированный анализ статических деревьев расширения можно выполнить с помощью потенциального метода . Определять:

Φ будет иметь тенденцию быть высоким для плохо сбалансированных деревьев и низким для хорошо сбалансированных деревьев.

Чтобы применить метод потенциала , мы сначала вычисляем ΔΦ: изменение потенциала, вызванное операцией расширения. Мы проверяем каждый случай отдельно. Обозначим через ранг' ранговую функцию после операции. x, p и g — узлы, на которые влияет операция вращения (см. рисунки выше).

Зиг-шаг

Зигзагообразный шаг

Зигзагообразный шаг

Амортизированная стоимость любой операции равна ΔΦ плюс фактическая стоимость. Фактическая стоимость любой зигзагообразной или зигзагообразной операции равна 2, поскольку необходимо сделать два вращения. Следовательно:

При суммировании по всей операции расширения это число увеличивается до 1 + 3(rank(root)−rank( x )), что равно O(log n ), поскольку мы используем операцию Zig не более одного раза, а амортизированная стоимость zig равна большинство 1+3(ранг'( x ) − ранг( x )).

Итак, теперь мы знаем, что общее амортизированное время для последовательности из m операций равно:

Чтобы перейти от амортизированного времени к фактическому времени, мы должны добавить уменьшение потенциала от начального состояния до выполнения какой-либо операции (Φ i ) к конечному состоянию после завершения всех операций (Φ f ).

где обозначение большого O может быть оправдано тем фактом, что для каждого узла x минимальный ранг равен 0, а максимальный ранг равен log( n ).

Теперь мы можем, наконец, ограничить фактическое время:

Взвешенный анализ

Приведенный выше анализ можно обобщить следующим образом.

Применяется тот же анализ, и амортизированная стоимость операции расширения снова равна:

где W — сумма всех весов.

Уменьшение от начального потенциала к конечному ограничено:

поскольку максимальный размер любого отдельного узла равен W , а минимальный — w(x) .

Следовательно, фактическое время ограничено:

Теоремы производительности

Существует несколько теорем и гипотез относительно времени выполнения в наихудшем случае для выполнения последовательности S из m доступов в расширенном дереве, содержащем n элементов.

Теорема о балансе  .  Стоимость выполнения последовательности S равна .

Доказательство

Возьмите постоянный вес, например ⁠ ⁠ для каждого узла x . Тогда ⁠ ⁠ .

Эта теорема подразумевает, что деревья расширения работают так же хорошо, как статически сбалансированные деревья двоичного поиска, для последовательностей по крайней мере n обращений. [1]

Теорема статической оптимальности  .  Пусть будет количество раз, когда к элементу x обращаются в S . Если к каждому элементу обращаются хотя бы один раз, то стоимость выполнения S равна

Доказательство

Позволять . Затем .

Эта теорема подразумевает, что деревья расширения работают так же хорошо, как оптимальное статическое двоичное дерево поиска, для последовательностей по крайней мере n обращений. [7] Они тратят меньше времени на более частые вещи. [1] Другой способ сформулировать тот же результат состоит в том, что во входных последовательностях, где элементы выбираются независимо случайным образом из неравномерного распределения вероятностей для n элементов, амортизированная ожидаемая ( средняя случай ) стоимость каждого доступа пропорциональна энтропия распределения. [8]

Теорема о статическом пальце  .  Предположим, что элементы пронумерованы от 1 до n в порядке возрастания. Пусть f — любой фиксированный элемент («палец»). Тогда стоимость выполнения S составит .

Доказательство

Позволять . Тогда . Чистое падение потенциала составляет O ( n log n ), поскольку вес любого предмета составляет не менее . [1]

Теорема о динамическом пальце  .  Предположим, что «палец» для каждого шага доступа к элементу y — это элемент, к которому обращались на предыдущем шаге, x . Стоимость выполнения S составляет . [9] [10]

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

Доказательство

Позволять . Обратите внимание, что здесь веса меняются во время последовательности. Однако последовательность весов по-прежнему является перестановкой . Итак, как и прежде . Чистое падение потенциала равно O ( n log n ).

Эта теорема эквивалентна деревьям расширения, имеющим независимую от ключа оптимальность . [1]

Теорема сканирования  .  Также известна как теорема последовательного доступа или теорема очереди . Доступ к n элементам дерева расширения в симметричном порядке занимает время O ( n ), независимо от исходной структуры дерева расширения. [11] Самая точная верхняя граница, доказанная на данный момент, равна . [12]

Гипотеза динамической оптимальности

Нерешенная задача в информатике :
Работают ли расширенные деревья так же хорошо, как любой другой алгоритм двоичного дерева поиска?

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

Гипотеза динамической оптимальности: [1] Пусть будет любым алгоритмом двоичного дерева поиска, который получает доступ к элементу, проходя путь от корня до, затрачивая , и который между обращениями может совершать любые вращения в дереве по цене 1 за поворот. Пусть будет стоимость выполнения последовательности доступов. Тогда стоимость выполнения того же доступа к расширенному дереву составит .

Есть несколько следствий гипотезы динамической оптимальности, которые остаются недоказанными:

Гипотеза обхода: [1] Пусть и будут двумя деревьями расширения, содержащими одни и те же элементы. Пусть — последовательность, полученная путем посещения элементов в предварительном порядке (т. е. порядок поиска в глубину). Общая стоимость выполнения последовательности доступов составляет .
Гипотеза о деке: [11] [13] [14] Пусть это последовательность двусторонних операций с очередью (push, pop, inject, eject). Тогда стоимость выполнения на дереве расширения составит .
Гипотеза о расщеплении: [6] Пусть будет любой перестановкой элементов дерева расширения. Тогда стоимость удаления элементов в заказе составит .

Варианты

Чтобы уменьшить количество операций реструктуризации, можно заменить расширение на полурасширение , при котором элемент растягивается только наполовину в сторону корня. [1] [2]

Другой способ уменьшить реструктуризацию — выполнить полное расширение, но только в некоторых операциях доступа — только когда путь доступа длиннее порогового значения или только в первых m операциях доступа. [1]

CBTree дополняет дерево расширения счетчиками доступа на каждом узле и нечасто использует их для реструктуризации. Вариант CBTree, называемый LazyCBTree, выполняет не более одного поворота при каждом поиске. Это используется вместе с оптимистической схемой ручной проверки для создания параллельного самонастраивающегося дерева. [15]

Используя методы сжатия указателей [16] , можно построить краткое дерево расширения.

Смотрите также

Примечания

  1. ^ abcdefghijklmn Слеатор и Тарьян 1985.
  2. ^ abc Бринкманн, Деграер и Де Луф 2009.
  3. ^ Гудрич, Тамассия и Гольдвассер 2014.
  4. ^ Альберс и Карпински 2002.
  5. ^ Аллен и Манро 1978.
  6. ^ Аб Лукас 1991.
  7. ^ Кнут 1997, с. 478
  8. ^ Гринберг и др. (1995).
  9. ^ Коул и др. 2000.
  10. ^ Коул 2000.
  11. ^ аб Тарьян 1985.
  12. ^ Элмасри 2004.
  13. ^ Петти 2008.
  14. ^ Сундар 1992.
  15. ^ Афек и др. 2014 год
  16. ^ Бендер и др. 2023.

Рекомендации

Внешние ссылки