Левосторонняя куча

Материал из Викиконспекты
Перейти к: навигация, поиск

Определение

Левосторонние деревья были изобретены Кларком Крейном (Clark Allan Crane), свое название они получили из-за того, что левое поддерево обычно длиннее правого.

Определение:
Левосторонняя куча (leftist heap) — двоичное левосторонее дерево (не обязательно сбалансированное), но с соблюдением порядка кучи (heap order).
Лемма (1):
В двоичном дереве с [math]n[/math] вершинами существует свободная позиция на глубине не более [math]\log{n}[/math].
Доказательство:
[math]\triangleright[/math]
Если бы все свободные позиции были на глубине более логарифма, то мы получили бы полное дерево с количеством вершин более [math]n[/math].
[math]\triangleleft[/math]
Левосторонняя куча

Левосторонняя куча накладывает на двоичное дерево дополнительное условие. Ближайшая свободная позиция должна быть самой правой позицией в дереве. То есть помимо обычного условия кучи выполняется следующее:


Определение:
Условие левосторонней кучи. Пусть [math]dist(u)[/math] — расстояние от вершины [math]u[/math] до ближайшей свободной позиции в ее поддереве. У пустых позиций [math]dist = 0[/math]. Тогда потребуем для любой вершины [math]u : dist(u.L)\ge dict(u.R)[/math].


Если для какой- то вершины это свойство не выполняется, то это легко устраняется: можно за [math]O(1)[/math] поменять местами левого и правого ребенка, что не повлияет на порядок кучи.

Поддерживаемые операции

merge

Слияние двух куч.

 merge(x, y) // x, y — корни двух деревьев
   if x == [math] \varnothing [/math]: return y
   if y == [math] \varnothing [/math]: return x
   if y.key < x.key:
     x [math]\leftrightarrow[/math] y
   // Воспользуемся тем, что куча левосторонняя. Правая ветка — самая короткая и не длиннее логарифма.
   // Пойдем направо и сольем правое поддерево с у.
   x.R = merge(x.R, y)
   // Могло возникнуть  нарушение левостороннести кучи.
   if dist(x.R) > dist(x.L):
     x.L [math]\leftrightarrow[/math] x.R
   dist(x) = min(dist(x.L), dist(x.R)) + 1 // пересчитаем расстояние до ближайшей свободной позиции
   return x
   // Каждый раз идем из уже существующей вершины только в правое поддерево — не более логарифма вызовов (по лемме)

Левосторонняя куча относится к сливаемым кучам: остальные операции легко реализуются с помощью операции слияния.

insert

Вставка новой вершины в дерево. Новое левостороннее дерево, состоящее из одной вершины, сливается с исходным.

extractMin

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

delete

Аналогично удаляется любой элемент — на его место ставится результат слияния его детей. Но так просто любой элемент удалить нельзя — на пути от этого элемента к корню может нарушиться левостороннесть кучи. А до корня мы дойти не можем, так как элемент может находиться на линейной глубине. Поэтому удаление реализуется с помощью [math]decrease key[/math]. Уменьшаем ключ до [math]-\infty[/math], затем извлекаем минимальное значение.

decreaseKey

Лемма (2):
У левостороннего дерева с правой ветвью длинны [math]h[/math] количество узлов [math]n \ge 2^{h} - 1[/math].
Доказательство:
[math]\triangleright[/math]

Индукция по h.

При [math]h = 1[/math] — верно.

При [math]h \gt 1[/math] левое и правое поддеревья исходного дерева левосторонние, а [math]dist[/math] от их корней больше либо равен [math]h - 1[/math].

По индукции число узлов в каждом из них больше или равно [math]2^{h - 1} - 1[/math], тогда во все дереве [math]n \ge (2^{h - 1} - 1) + (2^{h - 1} - 1) + 1 = 2^{h} - 1[/math] узлов.
[math]\triangleleft[/math]

Алгоритм

  • Найдем узел [math]x[/math], вырежем поддерево с корнем в этом узле.
  • Пройдем от предка вырезанной вершины, при этом пересчитывая [math]dist[/math]. Если [math]dist[/math] левого сына вершины меньше [math]dist[/math] правого, то меняем местами поддеревья.
  • Уменьшаем ключ данного узла и сливаем два дерева: исходное и вырезанное.
Лемма (3):
Нужно транспонировать не более [math]\log{n}[/math] поддеревьев.
Доказательство:
[math]\triangleright[/math]
Длина пути от вершины до корня может быть и [math]O(n)[/math], но нам не нужно подниматься до корня — достаточно подняться до вершины, у которой свойство левосторонней кучи уже выполнено. Транспонируем только если [math]dist(x.L) \lt dist(x.R)[/math], но [math]dist(x.R) \le \log{n}[/math]. На каждом шаге, если нужно транспонируем и увеличиваем [math]dist++[/math], тогда [math]dist[/math] увеличится до [math]\log{n}[/math] и обменов уже не надо будет делать.
[math]\triangleleft[/math]

Таким образом, мы восстановили левостороннесть кучи за [math]O(\log{n})[/math]. Поэтому асимптотика операции [math] decreaseKey [/math][math]O(\log{n})[/math].


Построение кучи за O(n)

Храним список левосторонних куч. Пока их количество больше [math]1[/math], из начала списка достаем две кучи, сливаем их и кладем в конец списка.

Покажем, почему это будет работать за [math] O(n) [/math].

Пусть на [math] i [/math]-ом шаге алгоритма в нашем списке остались только кучи размера [math] 2^i [/math]. На нулевом шаге у нас [math] n [/math] куч из одного элемента, и на каждом следующем количество куч будет уменьшаться вдвое, а число вершин в куче будет увеличиваться вдвое. Слияние двух куч из [math] n_i [/math] элементов выполняется за [math] O(\log{n_i}) [/math]. Поэтому построение будет выполняться за

[math] \sum\limits_{i = 1}^{\left\lceil \log{n} \right\rceil} \genfrac{}{}{}{0}{n \cdot \log{n_i}}{2^i} = n \cdot \sum\limits_{i = 1}^{\left\lceil \log{n} \right\rceil} \genfrac{}{}{}{0}{\log{2^i}}{2^i} = n \cdot \sum\limits_{i = 1}^{\left\lceil \log{n} \right\rceil} \genfrac{}{}{}{0}{i}{2^i} [/math]

Покажем, что сумма — [math] O(1) [/math], тогда и алгоритм будет выполняться за [math] O(n) [/math]. Найдём сумму ряда, заменив его на эквивалентный функциональный:

[math] \sum\limits_{i = 1}^{\left\lceil \log{n} \right\rceil} \genfrac{}{}{}{0}{i}{2^i} \lt \sum\limits_{i = 1}^{\infty } \genfrac{}{}{}{0}{i}{2^i} \\ f(x) = \sum\limits_{i = 1}^{\infty } \Bigl. i \cdot x^i \Bigr|_{x = \frac{1}{2}}, ~\genfrac{}{}{}{0}{f(x)}{x} = \sum\limits_{i = 1}^{\infty } i \cdot x^{i - 1}, ~\int\genfrac{}{}{}{0}{f(x)}{x} = \sum\limits_{i = 1}^{\infty } x^i =~\genfrac{}{}{}{0}{1}{1 - x} - 1, \\ ~\genfrac{}{}{}{0}{f(x)}{x} = (\genfrac{}{}{}{0}{1}{1 - x} - 1)' = \genfrac{}{}{}{0}{1}{(1 - x)^2}, ~f(x) = \genfrac{}{}{}{0}{x}{(1 - x)^2} [/math]

После подстановки [math] x = \genfrac{}{}{}{0}{1}{2} [/math] получаем, что сумма равна [math] 2 [/math]. Следовательно, построение кучи таким образом произойдёт за [math] O(n) [/math].

Преимущества левосторонней кучи

Нигде не делается уничтожающих присваиваний. Не создается новых узлов в [math]merge[/math]. Эта реализация слияния является функциональной — ее легко реализовать на функциональном языке программирования. Также данная реалзация [math]merge[/math] является персистентной.

Ссылки

1. Лекция А. С. Станкевича

2. Левосторонние кучи. Интуит.

3. Wikipedia — Leftist tree