Двоичная куча — различия между версиями
Sergej (обсуждение | вклад) (→Построение кучи за O(N)) |
м (rollbackEdits.php mass rollback) |
||
(не показаны 103 промежуточные версии 22 участников) | |||
Строка 1: | Строка 1: | ||
==Определение== | ==Определение== | ||
− | |||
{{Определение | {{Определение | ||
|definition= | |definition= | ||
− | '''Двоичная куча''' или '''пирамида''' — такое двоичное [[Дерево, эквивалентные определения|подвешенное дерево]], для которого выполнены следующие три условия: | + | '''Двоичная куча''' или '''пирамида''' (англ. ''Binary heap'') — такое двоичное [[Дерево, эквивалентные определения|подвешенное дерево]], для которого выполнены следующие три условия: |
− | * Значение в любой вершине не | + | * Значение в любой вершине не больше (если куча для минимума), чем значения её потомков. |
* На <tex>i</tex>-ом слое <tex>2^i</tex> вершин, кроме последнего. Слои нумеруются с нуля. | * На <tex>i</tex>-ом слое <tex>2^i</tex> вершин, кроме последнего. Слои нумеруются с нуля. | ||
* Последний слой заполнен слева направо (как показано на рисунке) | * Последний слой заполнен слева направо (как показано на рисунке) | ||
}} | }} | ||
− | + | [[Файл:Min_heap.png|thumb|325px|Пример кучи для минимума]] | |
− | [[Файл: | + | [[Файл:Min_heap_array.png|thumb|325px|Хранение кучи в массиве, красная стрелка {{---}} левый сын, зеленая {{---}} правый]] |
− | + | Удобнее всего двоичную кучу хранить в виде массива <tex>a[0..n-1]</tex>, у которого нулевой элемент, <tex>a[0]</tex> — элемент в корне, а потомками элемента <tex>a[i]</tex> являются <tex>a[2i+1]</tex> и <tex>a[2i+2]</tex>. Высота кучи определяется как высота двоичного дерева. То есть она равна количеству рёбер в самом длинном простом пути, соединяющем корень кучи с одним из её листьев. Высота кучи есть <tex>O(\log{n})</tex>, где <tex>n</tex> — количество узлов дерева. | |
− | Удобнее всего двоичную кучу хранить в виде массива <tex> | ||
Чаще всего используют кучи для минимума (когда предок не больше детей) и для максимума (когда предок не меньше детей). | Чаще всего используют кучи для минимума (когда предок не больше детей) и для максимума (когда предок не меньше детей). | ||
− | Двоичные кучи используют, например, для того, чтобы извлекать минимум из набора чисел за <tex>O(\log{ | + | Двоичные кучи используют, например, для того, чтобы извлекать минимум из набора чисел за <tex>O(\log{n})</tex>. Они являются частным случаем приоритетных очередей. |
==Базовые процедуры== | ==Базовые процедуры== | ||
Строка 22: | Строка 20: | ||
===Восстановление свойств кучи=== | ===Восстановление свойств кучи=== | ||
− | Если в куче изменяется один из элементов, то она может перестать удовлетворять свойству упорядоченности. Для восстановления этого свойства служат процедуры | + | Если в куче изменяется один из элементов, то она может перестать удовлетворять свойству упорядоченности. Для восстановления этого свойства служат процедуры <tex> \mathrm {siftDown} </tex> (просеивание вниз) |
− | + | и <tex> \mathrm {siftUp} </tex> (просеивание вверх). | |
− | |||
− | |||
− | |||
− | <code> | + | ====siftDown==== |
− | + | Если значение измененного элемента увеличивается, то свойства кучи восстанавливаются функцией <tex> \mathrm {siftDown} </tex>. | |
− | + | ||
− | + | Работа процедуры: если <tex>i</tex>-й элемент меньше, чем его сыновья, всё поддерево уже является кучей, и делать ничего не надо. В противном случае меняем местами <tex>i</tex>-й элемент с наименьшим из его сыновей, после чего выполняем <tex> \mathrm {siftDown} </tex> для этого сына. | |
− | + | Процедура выполняется за время <tex>O(\log{n})</tex>. | |
− | + | ||
− | + | <code style="display:inline-block"> | |
− | + | '''function''' siftDown(i : '''int'''): | |
− | + | '''while''' 2 * i + 1 < a.heapSize <font color = "green">// heapSize {{---}} количество элементов в куче</font> | |
− | + | left = 2 * i + 1 <font color = "green">// left {{---}} левый сын</font> | |
− | + | right = 2 * i + 2 <font color = "green">// right {{---}} правый сын</font> | |
− | + | j = left | |
− | + | '''if''' right < a.heapSize '''and''' a[right] < a[left] | |
− | + | j = right | |
− | + | '''if''' a[i] <= a[j] | |
− | + | '''break''' | |
− | + | swap(a[i], a[j]) | |
− | + | i = j | |
</code> | </code> | ||
− | |||
− | + | ====siftUp==== | |
− | + | Если значение измененного элемента уменьшается, то свойства кучи восстанавливаются функцией <tex> \mathrm {siftUp} </tex>. | |
− | |||
− | < | + | Работа процедуры: если элемент больше своего отца, условие 1 соблюдено для всего дерева, и больше ничего делать не нужно. Иначе, мы меняем местами его с отцом. После чего выполняем <tex> \mathrm {siftUp} </tex> |
− | + | для этого отца. Иными словами, слишком маленький элемент всплывает наверх. | |
− | + | Процедура выполняется за время <tex>O(\log{n})</tex>. | |
− | + | <code style="display:inline-block"> | |
− | + | '''function''' siftUp(i : '''int'''): | |
− | + | '''while''' a[i] < a[(i - 1) / 2] <font color = "green">// i <tex>==</tex> 0 {{---}} мы в корне</font> | |
+ | swap(a[i], a[(i - 1) / 2]) | ||
+ | i = (i - 1) / 2 | ||
</code> | </code> | ||
===Извлечение минимального элемента=== | ===Извлечение минимального элемента=== | ||
− | Выполняет извлечение минимального элемента из кучи за время <tex>O(\log{ | + | Выполняет извлечение минимального элемента из кучи за время <tex>O(\log{n})</tex>. |
Извлечение выполняется в четыре этапа: | Извлечение выполняется в четыре этапа: | ||
# Значение корневого элемента (он и является минимальным) сохраняется для последующего возврата. | # Значение корневого элемента (он и является минимальным) сохраняется для последующего возврата. | ||
# Последний элемент копируется в корень, после чего удаляется из кучи. | # Последний элемент копируется в корень, после чего удаляется из кучи. | ||
− | # Вызывается | + | # Вызывается <math> \mathrm {siftDown} </math> для корня. |
# Сохранённый элемент возвращается. | # Сохранённый элемент возвращается. | ||
− | + | ||
− | + | '''int''' extractMin(): | |
− | + | '''int''' min = a[0] | |
− | + | a[0] = a[a.heapSize - 1] | |
− | + | a.heapSize = a.heapSize - 1 | |
− | + | siftDown(0) | |
− | + | '''return''' min | |
− | |||
===Добавление нового элемента=== | ===Добавление нового элемента=== | ||
− | Выполняет добавление элемента в кучу за время <tex>O(\log{ | + | Выполняет добавление элемента в кучу за время <tex>O(\log{n})</tex>. |
− | Добавление произвольного элемента в конец кучи, и восстановление свойства упорядоченности с помощью процедуры | + | Добавление произвольного элемента в конец кучи, и восстановление свойства упорядоченности с помощью процедуры <math> \mathrm {siftUp} </math>. |
− | <code> | + | <code style="display:inline-block"> |
− | insert(key) | + | '''function''' insert(key : '''int'''): |
− | + | a.heapSize = a.heapSize + 1 | |
− | + | a[a.heapSize - 1] = key | |
− | + | siftUp(a.heapSize - 1) | |
</code> | </code> | ||
− | ==Построение кучи за O( | + | ===Построение кучи за O(n) === |
{{Определение | definition = | {{Определение | definition = | ||
− | '''<tex> | + | '''<tex>D</tex>-куча''' {{---}} это куча, в которой у каждого элемента, кроме, возможно, элементов на последнем уровне, ровно <tex>d</tex> потомков. |
+ | }} | ||
+ | |||
+ | Дан массив <tex>a[0.. n - 1].</tex> Требуется построить <tex>d</tex>-кучу с минимумом в корне. Наиболее очевидный способ построить такую кучу из неупорядоченного массива {{---}} сделать нулевой элемент массива корнем, а дальше по очереди добавить все его элементы в конец кучи и запускать от каждого добавленного элемента <math>\mathrm {siftUp}</math>. Временная оценка такого алгоритма <tex> O(n\log{n})</tex>. Однако можно построить кучу еще быстрее — за <tex> O(n) </tex>. | ||
+ | |||
+ | Представим, что в массиве хранится дерево (<tex>a[0] - </tex> корень, а потомками элемента <tex>a[i]</tex> являются <tex>a[di+1]...a[di+d]</tex>). Сделаем <tex> \mathrm {siftDown} </tex> для вершин, имеющих хотя бы одного потомка: от <tex dpi=140>\dfrac{n}{d}</tex> до <tex>0</tex>,{{---}} так как поддеревья, состоящие из одной вершины без потомков, уже упорядочены. | ||
+ | {{Лемма | ||
+ | |statement= На выходе получим искомую кучу. | ||
+ | |proof= До вызова <tex> \mathrm {siftDown} </tex> для вершины, ее поддеревья являются кучами. После выполнения <tex> \mathrm {siftDown} </tex> эта вершина с ее поддеревьями будут также являться кучей. Значит, после выполнения всех <tex> \mathrm {siftDown} </tex> получится куча. | ||
}} | }} | ||
− | |||
− | |||
{{Лемма | {{Лемма | ||
− | |statement= Время работы этого алгоритма <tex> O( | + | |statement= Время работы этого алгоритма <tex> O(n) </tex>. |
− | |proof= | + | |proof= Число вершин на высоте <tex>h</tex> в куче из <tex>n</tex> элементов не превосходит <tex dpi = "160"> \left \lceil \frac{n}{d^h} \right \rceil </tex>. Высота кучи не превосходит <tex> \log_{d}n </tex>. Обозначим за <tex> H </tex> высоту дерева, тогда время построения не превосходит |
− | Число вершин на высоте <tex>h</tex> в куче из <tex>n</tex> элементов не превосходит <tex dpi = "160"> \left \lceil \frac{n}{d^h} \right \rceil </tex>. Высота кучи не превосходит <tex> \log_{d} n </tex>. Обозначим за <tex> H </tex> высоту дерева, тогда время построения не превосходит | + | |
+ | <tex dpi = "160"> \sum_{h = 1}^H \limits\frac{n}{d^h} \cdot d </tex> <tex dpi = "150"> \cdot h </tex> <tex dpi = "160"> = n \cdot d \cdot {\sum_{h = 1}^H \limits}\frac{h}{d^h}. </tex> | ||
+ | |||
+ | Докажем вспомогательную лемму о сумме ряда. | ||
− | |||
− | |||
{{Лемма | {{Лемма | ||
|statement= <tex dpi = "160"> {\sum_{h = 1}^\infty \limits}\frac{h}{d^h} = \frac{d}{(d - 1)^2} . </tex> | |statement= <tex dpi = "160"> {\sum_{h = 1}^\infty \limits}\frac{h}{d^h} = \frac{d}{(d - 1)^2} . </tex> | ||
|proof= | |proof= | ||
− | Обозначим за <tex> | + | Обозначим за <tex>s</tex> сумму ряда. Заметим, что |
<tex dpi = "160"> \frac{n}{d^n} = \frac{1}{d} \cdot \frac{n - 1}{d ^{n - 1}} + \frac{1}{d^n}. </tex> | <tex dpi = "160"> \frac{n}{d^n} = \frac{1}{d} \cdot \frac{n - 1}{d ^{n - 1}} + \frac{1}{d^n}. </tex> | ||
− | <tex dpi = "160">{\sum_{n = 1}^\infty \limits}\frac{1}{d^n} | + | <tex dpi = "160">{\sum_{n = 1}^\infty \limits}\frac{1}{d^n}</tex> {{---}} это сумма бесконечной убывающей геометрической прогрессии, и она равна <tex dpi = "160"> |
\frac{\frac{1}{d}}{1 - \frac{1}{d}} = \frac{1}{d - 1}. </tex> | \frac{\frac{1}{d}}{1 - \frac{1}{d}} = \frac{1}{d - 1}. </tex> | ||
− | Получаем <tex> | + | Получаем <tex>s</tex> <tex dpi = "160" >=\frac{1}{d}</tex> <tex>\cdot s +</tex> <tex dpi = "160" > \frac{1}{d - 1}. </tex> Откуда <tex>s</tex> <tex dpi = "160"> = \frac{d}{(d - 1)^2}. </tex> |
}} | }} | ||
− | |||
− | |||
− | + | Подставляя в нашу формулу результат леммы, получаем <tex >n</tex> <tex dpi = "160">\cdot (\frac {d}{d - 1})^2 </tex> <tex> \leqslant 4 \cdot n </tex> <tex>=O(n).</tex> | |
}} | }} | ||
− | == | + | Псевдокод алгоритма: |
− | + | <code style="display:inline-block"> | |
− | * [ | + | '''function''' buldHeap(): |
+ | '''for''' i = a.heapSize / 2 '''downto''' 0 | ||
+ | siftDown(i) | ||
+ | </code> | ||
+ | |||
+ | ===Слияние двух куч=== | ||
+ | Даны две кучи <tex>a</tex> и <tex>b</tex>, размерами <tex>n</tex> и <tex>m</tex>, требуется объединить эти две кучи. | ||
+ | ====Наивная реализация==== | ||
+ | Поочередно добавим все элементы из <tex>b</tex> в <tex>a</tex>. Время работы {{---}} <tex>O(m \log(n+m))</tex>. | ||
+ | <code style="display:inline-block"> | ||
+ | '''function''' merge(a, b : '''Heap'''): | ||
+ | '''while''' b.heapSize > 0 | ||
+ | a.insert(b.extractMin()) | ||
+ | </code> | ||
+ | |||
+ | ====Реализация с помощью построения кучи==== | ||
+ | Добавим все элементы кучи <tex>b</tex> в конец массива <tex>a</tex>, после чего вызовем функцию построения кучи. Процедура выполняется за время <tex>O(n + m)</tex>. | ||
+ | |||
+ | <code style="display:inline-block"> | ||
+ | '''function''' merge(a, b : '''Heap'''): | ||
+ | '''for''' i = 0 '''to''' b.heapSize - 1 | ||
+ | a.heapSize = a.heapSize + 1 | ||
+ | a[a.heapSize - 1] = b[i] | ||
+ | a.heapify() | ||
+ | </code> | ||
+ | |||
+ | ===Поиск k-ого элемента (очень коряво расписано с неверными индексами)=== | ||
+ | Требуется найти <tex>k</tex>-ый по величине элемент в куче. | ||
+ | |||
+ | # Создаем новую кучу, в которой будем хранить пару <tex>\langle \mathtt{value}, \mathtt{index} \rangle</tex>, где <tex>\mathtt{value}</tex> {{---}} значение элемента, а <tex>\mathtt{index}</tex> {{---}} индекс элемента в основном массиве, и добавляем в нее корень кучи. | ||
+ | # Возьмем корень новой кучи и добавим её детей из основной кучи, после чего удалим корень. Проделаем этот шаг <tex>k - 1</tex> раз. | ||
+ | # В корне новой кучи будет находиться ответ. | ||
+ | |||
+ | Время работы алгоритма {{---}} <tex>O(k \log k)</tex>. | ||
+ | |||
+ | При <tex>n \lessapprox k \log k </tex> выгоднее запускать [[поиск k-ой порядковой статистики]]. | ||
+ | [[Файл:Min_heap_kth.png|thumb|center|650px|Пример при <tex>k = 5</tex>, красные {{---}} уже удаленные из кучи элементы, зеленые находятся в куче, а голубые {{---}} еще не рассмотрены.]] | ||
+ | |||
+ | == См. также == | ||
+ | * [[Биномиальная куча]] | ||
+ | * [[Фибоначчиева куча]] | ||
+ | * [[Левосторонняя куча]] | ||
+ | |||
+ | == Источники информации == | ||
+ | * [[wikipedia:ru:Двоичная куча|Википедия {{---}} Двоичная куча]] | ||
+ | * [[wikipedia:ru:Очередь с приоритетом|Википедия {{---}} Очередь с приоритетом]] | ||
+ | * [[wikipedia:en:Binary heap|Wikipedia {{---}} Binary heap]] | ||
+ | * [[wikipedia:en:Priority queue|Wikipedia {{---}} Priority queue]] | ||
[[Категория: Дискретная математика и алгоритмы]] | [[Категория: Дискретная математика и алгоритмы]] | ||
[[Категория: Приоритетные очереди]] | [[Категория: Приоритетные очереди]] | ||
+ | [[Категория: Структуры данных]] |
Текущая версия на 19:30, 4 сентября 2022
Содержание
Определение
Определение: |
Двоичная куча или пирамида (англ. Binary heap) — такое двоичное подвешенное дерево, для которого выполнены следующие три условия:
|
Удобнее всего двоичную кучу хранить в виде массива
, у которого нулевой элемент, — элемент в корне, а потомками элемента являются и . Высота кучи определяется как высота двоичного дерева. То есть она равна количеству рёбер в самом длинном простом пути, соединяющем корень кучи с одним из её листьев. Высота кучи есть , где — количество узлов дерева.Чаще всего используют кучи для минимума (когда предок не больше детей) и для максимума (когда предок не меньше детей).
Двоичные кучи используют, например, для того, чтобы извлекать минимум из набора чисел за
. Они являются частным случаем приоритетных очередей.Базовые процедуры
Восстановление свойств кучи
Если в куче изменяется один из элементов, то она может перестать удовлетворять свойству упорядоченности. Для восстановления этого свойства служат процедуры
(просеивание вниз) и (просеивание вверх).siftDown
Если значение измененного элемента увеличивается, то свойства кучи восстанавливаются функцией
.Работа процедуры: если
-й элемент меньше, чем его сыновья, всё поддерево уже является кучей, и делать ничего не надо. В противном случае меняем местами -й элемент с наименьшим из его сыновей, после чего выполняем для этого сына. Процедура выполняется за время .
function siftDown(i : int): while 2 * i + 1 < a.heapSize // heapSize — количество элементов в куче left = 2 * i + 1 // left — левый сын right = 2 * i + 2 // right — правый сын j = left if right < a.heapSize and a[right] < a[left] j = right if a[i] <= a[j] break swap(a[i], a[j]) i = j
siftUp
Если значение измененного элемента уменьшается, то свойства кучи восстанавливаются функцией
.Работа процедуры: если элемент больше своего отца, условие 1 соблюдено для всего дерева, и больше ничего делать не нужно. Иначе, мы меняем местами его с отцом. После чего выполняем
function siftUp(i : int):
while a[i] < a[(i - 1) / 2] // i
0 — мы в корне
swap(a[i], a[(i - 1) / 2])
i = (i - 1) / 2
Извлечение минимального элемента
Выполняет извлечение минимального элемента из кучи за время
. Извлечение выполняется в четыре этапа:- Значение корневого элемента (он и является минимальным) сохраняется для последующего возврата.
- Последний элемент копируется в корень, после чего удаляется из кучи.
- Вызывается для корня.
- Сохранённый элемент возвращается.
int extractMin(): int min = a[0] a[0] = a[a.heapSize - 1] a.heapSize = a.heapSize - 1 siftDown(0) return min
Добавление нового элемента
Выполняет добавление элемента в кучу за время
. Добавление произвольного элемента в конец кучи, и восстановление свойства упорядоченности с помощью процедуры .
function insert(key : int): a.heapSize = a.heapSize + 1 a[a.heapSize - 1] = key siftUp(a.heapSize - 1)
Построение кучи за O(n)
Определение: |
-куча — это куча, в которой у каждого элемента, кроме, возможно, элементов на последнем уровне, ровно потомков. |
Дан массив Требуется построить -кучу с минимумом в корне. Наиболее очевидный способ построить такую кучу из неупорядоченного массива — сделать нулевой элемент массива корнем, а дальше по очереди добавить все его элементы в конец кучи и запускать от каждого добавленного элемента . Временная оценка такого алгоритма . Однако можно построить кучу еще быстрее — за .
Представим, что в массиве хранится дерево (
корень, а потомками элемента являются ). Сделаем для вершин, имеющих хотя бы одного потомка: от до ,— так как поддеревья, состоящие из одной вершины без потомков, уже упорядочены.Лемма: |
На выходе получим искомую кучу. |
Доказательство: |
До вызова | для вершины, ее поддеревья являются кучами. После выполнения эта вершина с ее поддеревьями будут также являться кучей. Значит, после выполнения всех получится куча.
Лемма: | ||||||
Время работы этого алгоритма . | ||||||
Доказательство: | ||||||
Число вершин на высоте в куче из элементов не превосходит . Высота кучи не превосходит . Обозначим за высоту дерева, тогда время построения не превосходит
Докажем вспомогательную лемму о сумме ряда.
| ||||||
Псевдокод алгоритма:
function buldHeap(): for i = a.heapSize / 2 downto 0 siftDown(i)
Слияние двух куч
Даны две кучи
и , размерами и , требуется объединить эти две кучи.Наивная реализация
Поочередно добавим все элементы из
function merge(a, b : Heap): while b.heapSize > 0 a.insert(b.extractMin())
Реализация с помощью построения кучи
Добавим все элементы кучи
в конец массива , после чего вызовем функцию построения кучи. Процедура выполняется за время .
function merge(a, b : Heap): for i = 0 to b.heapSize - 1 a.heapSize = a.heapSize + 1 a[a.heapSize - 1] = b[i] a.heapify()
Поиск k-ого элемента (очень коряво расписано с неверными индексами)
Требуется найти
-ый по величине элемент в куче.- Создаем новую кучу, в которой будем хранить пару , где — значение элемента, а — индекс элемента в основном массиве, и добавляем в нее корень кучи.
- Возьмем корень новой кучи и добавим её детей из основной кучи, после чего удалим корень. Проделаем этот шаг раз.
- В корне новой кучи будет находиться ответ.
Время работы алгоритма —
.При поиск k-ой порядковой статистики.
выгоднее запускать