Binary Heap

Бинарная куча — структура данных на основе почти полного бинарного дерева,
в котором выполняется свойство кучи.

Чаще всего бинарную кучу используют для реализации:

очереди с приоритетом

Главная идея:

лучший элемент всегда находится в корне

Для min-heap лучший элемент — минимальный.

Для max-heap лучший элемент — максимальный.

Термины

heap          -> куча
binary heap   -> бинарная куча
root          -> корень
parent        -> родитель
left child    -> левый ребёнок
right child   -> правый ребёнок
min-heap      -> куча с минимумом в корне
max-heap      -> куча с максимумом в корне
sift up       -> поднять элемент вверх
sift down     -> опустить элемент вниз
heapify       -> построить кучу из массива

Бинарная куча задаётся тремя вещами:

1. почти полное бинарное дерево
2. свойство кучи
3. массивное представление дерева

Структура кучи

Структура отвечает на вопрос:

Как быстро получить минимальный или максимальный элемент?

Пример min-heap:

        1
      /   \
     3     5
    / \   /
   7   9 8

Минимальный элемент находится в корне:

root = 1

Та же куча в массиве:

heap = [1, 3, 5, 7, 9, 8]

Почти полное бинарное дерево

Бинарная куча хранится как почти полное бинарное дерево.

Это значит:

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

Пример корректной формы:

        1
      /   \
     3     5
    / \   /
   7   9 8

Пример некорректной формы для бинарной кучи:

        1
      /   \
     3     5
          / \
         8   9

Здесь на последнем уровне элементы не заполнены слева направо.

Свойство кучи

Min-Heap

В min-heap каждый родитель не больше своих детей:

parent <= child

Пример:

        1
      /   \
     3     5
    / \   /
   7   9 8

Для каждого ребра:

1 <= 3
1 <= 5
3 <= 7
3 <= 9
5 <= 8

Поэтому минимум находится в корне.

Max-Heap

В max-heap каждый родитель не меньше своих детей:

parent >= child

Пример:

        9
      /   \
     7     8
    / \   /
   1   3 5

Для каждого ребра:

9 >= 7
9 >= 8
7 >= 1
7 >= 3
8 >= 5

Поэтому максимум находится в корне.

Массивное представление

Бинарную кучу обычно хранят в массиве.

Для массива:

heap = [1, 3, 5, 7, 9, 8]

индексы:

index:  0  1  2  3  4  5
value:  1  3  5  7  9  8

Связи между индексами:

parent(i) = (i - 1) // 2
left(i)   = 2 * i + 1
right(i)  = 2 * i + 2

Пример:

i = 1
heap[i] = 3

parent(1) = 0 -> heap[0] = 1
left(1)   = 3 -> heap[3] = 7
right(1)  = 4 -> heap[4] = 9

Массив удобен потому, что не нужны явные ссылки на детей и родителей.

Базовые операции

Peek

Посмотреть корень:

peek(heap)

Для min-heap:

heap[0] -> минимальный элемент

Для max-heap:

heap[0] -> максимальный элемент

Сложность:

O(1)

Push

Добавить элемент в кучу:

push(heap, x)

Алгоритм:

1. добавить x в конец массива
2. поднять x вверх, пока свойство кучи нарушено

Подъём вверх называется:

sift up

Пример для min-heap:

heap = [1, 3, 5, 7, 9, 8]
push(2)

Сначала добавляем в конец:

[1, 3, 5, 7, 9, 8, 2]

2 меньше родителя 5, значит меняем их:

[1, 3, 2, 7, 9, 8, 5]

Теперь родитель 1 меньше 2, свойство кучи восстановлено.

Сложность:

O(log n)

Pop

Удалить и вернуть корень:

pop(heap)

Алгоритм:

1. сохранить корень как ответ
2. перенести последний элемент в корень
3. удалить последний элемент
4. опустить новый корень вниз, пока свойство кучи нарушено

Опускание вниз называется:

sift down

Пример для min-heap:

heap = [1, 3, 5, 7, 9, 8]
pop() -> 1

Переносим последний элемент в корень:

[8, 3, 5, 7, 9]

8 больше меньшего ребёнка 3, меняем:

[3, 8, 5, 7, 9]

8 больше меньшего ребёнка 7, меняем:

[3, 7, 5, 8, 9]

Свойство кучи восстановлено.

Сложность:

O(log n)

Heapify

Построить кучу из произвольного массива:

heapify(arr)

Пример:

arr = [7, 1, 5, 3, 9, 8]
heapify(arr)

После построения min-heap один из корректных вариантов:

[1, 3, 5, 7, 9, 8]

Сложность эффективного heapify:

O(n)

Это быстрее, чем добавлять n элементов по одному:

n * O(log n) = O(n log n)

Таблица сложностей

Операция Сложность
peek() O(1)
push(x) O(log n)
pop() O(log n)
heapify(arr) O(n)
поиск произвольного значения O(n)
удаление произвольного значения O(n)

Почему высота равна O(log n)

Бинарная куча — почти полное бинарное дерево.

На каждом уровне количество возможных узлов удваивается:

1, 2, 4, 8, 16, ...

Поэтому при n элементах высота дерева:

O(log n)

Операции sift up и sift down проходят только один путь по высоте дерева,
поэтому стоят O(log n).

Бинарная куча и отсортированный массив

Куча не хранит элементы полностью отсортированными.

Для min-heap верно только локальное свойство:

родитель <= дети

Но между соседними элементами массива полного порядка может не быть:

heap = [1, 3, 5, 7, 9, 8]

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

Главное преимущество кучи:

можно быстро получить лучший элемент без полной сортировки

Бинарная куча и очередь с приоритетом

Очередь с приоритетом — абстракция:

push элемент с приоритетом
pop элемент с лучшим приоритетом

Бинарная куча — один из способов эффективно реализовать эту абстракцию:

peek -> O(1)
push -> O(log n)
pop  -> O(log n)

То есть:

Priority Queue = что нужно уметь
Binary Heap    = как это можно хранить

Python

В Python бинарная куча доступна через модуль:

heapq

heapq работает как min-heap.

Пример:

import heapq

heap = []

heapq.heappush(heap, 5)
heapq.heappush(heap, 1)
heapq.heappush(heap, 3)

print(heap[0])              # 1
print(heapq.heappop(heap))  # 1
print(heapq.heappop(heap))  # 3
print(heapq.heappop(heap))  # 5

Построение кучи из массива:

import heapq

arr = [7, 1, 5, 3, 9, 8]
heapq.heapify(arr)

print(arr)
print(arr[0])  # минимальный элемент

Для max-heap часто используют отрицательные значения:

import heapq

heap = []

heapq.heappush(heap, -5)
heapq.heappush(heap, -1)
heapq.heappush(heap, -9)

print(-heapq.heappop(heap))  # 9
print(-heapq.heappop(heap))  # 5
print(-heapq.heappop(heap))  # 1

Типичные применения

очередь с приоритетом
Dijkstra
A*
Prim
top K elements
merge K sorted lists
heap sort
медиана потока чисел
симуляции событий

Формальная схема

BinaryHeap = {
  elements: heap[0..n-1],
  shape: almost complete binary tree,
  order: min-heap или max-heap,
  root: heap[0],
  size: n
}

Операции кучи изменяют или используют одну из четырёх сущностей:

корень
родитель
ребёнок
свойство кучи

Примеры:

push(x)  -> добавляет элемент в конец и делает sift up
pop()    -> удаляет корень и делает sift down
peek()   -> читает корень
heapify  -> восстанавливает свойство кучи для всего массива

Минимальная формулировка:

бинарная куча = почти полное бинарное дерево с быстрым доступом к минимуму или максимуму
Прокрутить вверх