kz-maxx

Куча

Объяснение, хранение в массиве, push, pop, top, up, down

Объяснение

Куча — это структура данных, которая быстро находит самый маленький или самый большой элемент.

Есть два основных вида кучи:

  • min-heap — сверху всегда минимальный элемент
  • max-heap — сверху всегда максимальный элемент
В этой странице мы реализуем min-heap. То есть h[1] всегда хранит минимальный элемент.
min-heap:
h[1] = самый маленький элемент

Куча похожа на бинарное дерево, но хранится в обычном массиве.

Как куча хранится в массиве?

Используем массив:

int h[N];
int sz;
  • h — массив кучи
  • sz — количество элементов в куче

Индексация начинается с 1. Это удобно, потому что легко находить родителей и детей.

Что ищем Формула
родитель вершины i i / 2
левый сын вершины i i * 2
правый сын вершины i i * 2 + 1

Пример

индексы: 1 2 3 4 5
h: 2 5 7 9 8

Это дерево можно представить так:

2
5 7
9 8

Для min-heap правило такое:

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

Например, 2 меньше своих детей 5 и 7. Значит наверху стоит минимальный элемент.

Какие операции нужны?

Операция Что делает
push(x) добавляет число x в кучу
top() возвращает минимальный элемент
pop() удаляет минимальный элемент
up(i) поднимает элемент вверх, если он меньше родителя
down(i) опускает элемент вниз, если он больше детей

Пример задачи

Нужно обработать команды для min-heap.

Команда Что делает
push x добавить число x
top вывести минимальный элемент
pop удалить минимальный элемент
size вывести размер кучи

Input

8
push 5
push 2
push 7
top
pop
top
push 1
top

Output

2
5
1

Полная реализация через массив

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int h[N];
int sz;

void up(int u) {
    while (u / 2 > 0 && h[u] < h[u / 2]) {
        swap(h[u], h[u / 2]);
        u /= 2;
    }
}

void down(int u) {
    int t = u;

    if (u * 2 <= sz && h[u * 2] < h[t]) {
        t = u * 2;
    }

    if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) {
        t = u * 2 + 1;
    }

    if (t != u) {
        swap(h[u], h[t]);
        down(t);
    }
}

void push(int x) {
    sz++;
    h[sz] = x;
    up(sz);
}

int top() {
    return h[1];
}

void pop() {
    h[1] = h[sz];
    sz--;
    down(1);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int q;
    cin >> q;

    while (q--) {
        string op;
        cin >> op;

        if (op == "push") {
            int x;
            cin >> x;

            push(x);
        } else if (op == "top") {
            cout << top() << '\n';
        } else if (op == "pop") {
            pop();
        } else if (op == "size") {
            cout << sz << '\n';
        }
    }

    return 0;
}

Объяснение функций

1. up

void up(int u) {
    while (u / 2 > 0 && h[u] < h[u / 2]) {
        swap(h[u], h[u / 2]);
        u /= 2;
    }
}

up нужен после добавления нового элемента. Мы добавляем элемент в конец массива. Если он меньше родителя, поднимаем его выше.

Маленький пример

было: 2 5 7
push(1)
стало до up: 2 5 7 1

Новый элемент 1 стоит на позиции 4. Его родитель — позиция 2, там стоит 5. Так как 1 < 5, меняем их местами.

2 1 7 5

Теперь 1 стоит на позиции 2. Его родитель — позиция 1, там стоит 2. Так как 1 < 2, снова меняем.

1 2 7 5

Теперь 1 наверху. Куча снова правильная.

2. down

void down(int u) {
    int t = u;

    if (u * 2 <= sz && h[u * 2] < h[t]) {
        t = u * 2;
    }

    if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) {
        t = u * 2 + 1;
    }

    if (t != u) {
        swap(h[u], h[t]);
        down(t);
    }
}

down нужен после удаления верхнего элемента. Мы ставим последний элемент наверх. Потом опускаем его вниз, пока куча не станет правильной.

Маленький пример

было: 1 3 2 8 5
pop()

Удаляем 1. Последний элемент 5 ставим наверх.

стало до down: 5 3 2 8

У вершины 5 дети 3 и 2. Самый маленький ребенок — 2. Так как 2 < 5, меняем их местами.

стало: 2 3 5 8

Теперь куча снова правильная.

3. push

void push(int x) {
    sz++;
    h[sz] = x;
    up(sz);
}

push добавляет новый элемент в конец. Потом вызывает up, чтобы поднять его на правильное место.

было: 2 5 7
push(1)
сначала: 2 5 7 1
после up: 1 2 7 5

4. top

int top() {
    return h[1];
}

В min-heap минимальный элемент всегда находится в h[1]. Поэтому top просто возвращает h[1].

h = 2 5 7 9
top() = h[1] = 2

5. pop

void pop() {
    h[1] = h[sz];
    sz--;
    down(1);
}

pop удаляет минимальный элемент. Минимум лежит в h[1].

Чтобы удалить его:

  • ставим последний элемент на место первого
  • уменьшаем размер
  • вызываем down(1)
было: 1 3 2 8 5
pop()
ставим 5 наверх: 5 3 2 8
после down: 2 3 5 8

priority_queue в C++

В C++ уже есть готовая куча — priority_queue. По умолчанию она работает как max-heap.

Max-heap

priority_queue<int> q;

q.push(5);
q.push(2);
q.push(7);

cout << q.top();

Здесь top() вернет 7, потому что это максимальный элемент.

Min-heap

priority_queue<int, vector<int>, greater<int>> q;

q.push(5);
q.push(2);
q.push(7);

cout << q.top();

Здесь top() вернет 2, потому что это минимальный элемент.

На экзамене или олимпиаде важно понимать ручную реализацию через массив, но в обычных задачах часто можно использовать priority_queue.

Асимптотика

Операция Время
push O(log n)
pop O(log n)
top O(1)
size O(1)