Skip to main content

Introduction to Heaps

Editorial

The best on top, and nothing else

A sorted array hands you its smallest value at once, but every insert has to shift everything after it, O(n). Often you only ever need the smallest (or the largest): the next task, the closest point, the cheapest edge. A heap keeps just that promise. It's a complete binary tree in which every parent is at most its children, a min-heap, so the smallest value is at the root. Nothing else is ordered: not siblings, not levels, not the array. In exchange, adding a value and taking the smallest each cost O(log n):

Two dashed zones. As a tree: the min-heap drawn as shaded discs, 1 at the root in purple, ringed, with a pill reading the smallest; 3 and 2 below it in teal, with a pill between them reading siblings in any order; 7, 4, 5 and 8 on the bottom level in orange. As an array: the same values as tiles, 1 3 2 7 4 5 8, indexes 0 to 6, each coloured like its level, with brackets marking level 0, 1 and 2. Focal line: every parent is at most its children, so the smallest is on top. Title: only the top is promised.

Push sifts up, pop sifts down

The tree lives in an array, as Introduction to Binary Trees stored a complete tree: the children of index i are at 2i + 1 and 2i + 2, and its parent at (i − 1) / 2, rounded down. Both operations keep the tree complete first and then repair the order along one path:

  • Push puts the new value in the next free place, the end of the array, then sifts it up: while its parent is bigger, swap them.
  • Pop takes the root, moves the last value into the root's place, then sifts it down: while one of its children is smaller, swap it with the smaller child.

Each walks at most one path, root to leaf, so each is O(log n):

// A min-heap in a vector: the children of i are at 2i + 1 and 2i + 2, its parent at (i - 1) / 2.
struct MinHeap {
    vector<int> a;

    void push(int x) {
        a.push_back(x);
        size_t i = a.size() - 1;
        while (i > 0 && a[(i - 1) / 2] > a[i]) {        // sift up while the parent is bigger
            swap(a[i], a[(i - 1) / 2]);
            i = (i - 1) / 2;
        }
    }

    int pop() {
        int top = a[0];
        a[0] = a.back();                              // the last value takes the root, then sifts down
        a.pop_back();
        size_t i = 0;
        while (true) {
            size_t l = 2 * i + 1, r = 2 * i + 2, m = i;
            if (l < a.size() && a[l] < a[m]) m = l;
            if (r < a.size() && a[r] < a[m]) m = r;   // the smaller child, or the bigger one ends up above it
            if (m == i) break;
            swap(a[i], a[m]);
            i = m;
        }
        return top;
    }
};
class MinHeap:
    """A min-heap in a list: the children of i are at 2i + 1 and 2i + 2, its parent at (i - 1) // 2."""

    def __init__(self):
        self.a = []

    def push(self, x):
        a = self.a
        a.append(x)
        i = len(a) - 1
        while i > 0 and a[(i - 1) // 2] > a[i]:     # sift up while the parent is bigger
            p = (i - 1) // 2
            a[i], a[p] = a[p], a[i]
            i = p

    def pop(self):
        a = self.a
        top, last = a[0], a.pop()
        if a:
            a[0] = last                              # the last value takes the root, then sifts down
            i = 0
            while True:
                l, r, m = 2 * i + 1, 2 * i + 2, i
                if l < len(a) and a[l] < a[m]:
                    m = l
                if r < len(a) and a[r] < a[m]:       # the smaller child, or the bigger one ends up above it
                    m = r
                if m == i:
                    break
                a[i], a[m] = a[m], a[i]
                i = m
        return top
// A min-heap in a list: the children of i are at 2i + 1 and 2i + 2, its parent at (i - 1) / 2.
static class MinHeap {
    final List<Integer> a = new ArrayList<>();

    void push(int x) {
        a.add(x);
        int i = a.size() - 1;
        while (i > 0 && a.get((i - 1) / 2) > a.get(i)) {   // sift up while the parent is bigger
            Collections.swap(a, i, (i - 1) / 2);
            i = (i - 1) / 2;
        }
    }

    int pop() {
        int top = a.get(0);
        int last = a.remove(a.size() - 1);
        if (!a.isEmpty()) {
            a.set(0, last);                          // the last value takes the root, then sifts down
            int i = 0;
            while (true) {
                int l = 2 * i + 1, r = 2 * i + 2, m = i;
                if (l < a.size() && a.get(l) < a.get(m)) m = l;
                if (r < a.size() && a.get(r) < a.get(m)) m = r;   // the smaller child, or the bigger one ends up above it
                if (m == i) break;
                Collections.swap(a, i, m);
                i = m;
            }
        }
        return top;
    }
}
// A min-heap in an array: the children of i are at 2i + 1 and 2i + 2, its parent at (i - 1) >> 1.
// less(x, y) says x goes above y: smallest on top unless you pass another.
class MinHeap {
    constructor(less = (x, y) => x < y) { this.a = []; this.less = less; }

    push(x) {
        const a = this.a;
        a.push(x);
        let i = a.length - 1;
        while (i > 0 && this.less(a[i], a[(i - 1) >> 1])) {   // sift up while the parent is bigger
            const p = (i - 1) >> 1;
            [a[i], a[p]] = [a[p], a[i]];
            i = p;
        }
    }

    pop() {
        const a = this.a;
        const top = a[0], last = a.pop();
        if (a.length) {
            a[0] = last;                             // the last value takes the root, then sifts down
            let i = 0;
            while (true) {
                const l = 2 * i + 1, r = 2 * i + 2;
                let m = i;
                if (l < a.length && this.less(a[l], a[m])) m = l;
                if (r < a.length && this.less(a[r], a[m])) m = r;   // the smaller child, or the bigger ends up above it
                if (m === i) break;
                [a[i], a[m]] = [a[m], a[i]];
                i = m;
            }
        }
        return top;
    }
}

Each pushed 5, 2, 8, 1, 9, 3, printed the array [1, 2, 3, 5, 9, 8] (a heap, not sorted: 9 sits before 8), and then popped 1 2 3 5 8 9, smallest first. Step through a push:

Pushing 1 into [2, 4, 3, 7, 8, 5] — The array under the tree is the same heap. Indigo is the value being pushed; amber, the parent it just swapped with.

And a pop:

Popping [1, 2, 3, 4, 8, 5, 7] — Green is the value that will come off, then the value that settles; indigo, the one sifting down; amber, the child it swapped with.

Sifting down has to pick the smaller child. Swap with the bigger one and it ends up above the smaller, which breaks the rule at once:

Three dashed zones, each a small tree of shaded discs over the same three values as an array of tiles. First, 9 just took the root: 9 in purple over children 2 and 5, with a pill reading 9 has to sink. Second, swap the smaller, 2: 2 on top in green over 9 and 5, with a green card and a tick reading order holds. Third, swap the bigger, 5: 5 on top in red over 2 and 9, the edge from 5 down to 2 drawn thick and red, with a red card and a cross reading 5 above 2: broken, and the broken pair red in its array. Focal line: swap with the smaller child, or the bigger one lands above it. Title: pop swaps with the smaller child.

Below, you do the swaps. Push each value and lift it while its parent is bigger; then pop the heap empty, sinking each new root toward its smaller child. A wrong swap is refused, and the popped values come out sorted.

Sift Up, Sift Down: you make every swap

Building a heap in O(n)

To heap n values you could push them one by one, O(n log n). Building it bottom-up is cheaper: put the values in an array as they are, then sift down every parent, from the last one back to the root. When a node sifts down, both its subtrees are already heaps:

def heapify(a):
    """Bottom-up: sift down every parent, from the last one to the root."""
    for start in range(len(a) // 2 - 1, -1, -1):
        i = start
        while True:
            l, r, m = 2 * i + 1, 2 * i + 2, i
            if l < len(a) and a[l] < a[m]:
                m = l
            if r < len(a) and a[r] < a[m]:
                m = r
            if m == i:
                break
            a[i], a[m] = a[m], a[i]
            i = m

On [9, 8, 7, 6, 5, 4, 3] it gave [3, 5, 4, 6, 8, 9, 7]. Why O(n): half the nodes are leaves and never move, a quarter can sink one level, an eighth two, and so on. That sum stays under 2n, while a push can climb the whole height:

A dashed zone holds a complete tree of 15 nodes in four levels, shaded discs coloured by level; a second zone titled how far each level sinks gives each level a tile with its count, joined to its level by a dotted line: 1 node, sink at most 3 levels each; 2 nodes, at most 2; 4 nodes, at most 1; 8 leaves, never move. Under both, a green card with a tick reads at most 11 swaps for 15 values. Focal line: the work is where the nodes are few, so building costs under 2n. Title: most nodes sit near the bottom.

Counted on decreasing values, where every push climbs to the root:

ValuesSwaps for n pushesSwaps to build bottom-up
10,000113,6319,992
100,0001,468,94699,990

With Python's own heapq, a million decreasing values took 418.0 ms pushed one by one and 30.2 ms with heapify.

Heaps in your language

LanguageHeapOrder by defaultOther order
Pythonheapq functions on a listsmallest on toppush negatives, or tuples
JavaPriorityQueuesmallest on topa Comparator, such as Comparator.reverseOrder()
C++priority_queuelargest on topgreater<int> for the smallest
JavaScriptnone built inwrite one, as abovea comparison, such as (x, y) => x > y
  • Python: heapq works on a plain list, heap[0] is the smallest, and heapify is O(n). heappushpop pushes then pops; heapreplace pops then pushes.
  • Java: poll and offer are O(log n), but remove(x) and contains(x) walk the whole queue, and printing or iterating a PriorityQueue shows it in heap order, not sorted.
  • C++: priority_queue keeps the largest on top unless you pass greater<int>; top and pop are separate calls.
  • JavaScript: there is no heap; sorting an array on every push costs O(n) or more each time. Pairs such as [value, list] need a comparison: < on two arrays compares them as text, so [10, 0] < [9, 1] is true.
// A max-heap by default; greater<int> turns it into a min-heap.
void libraryHeaps() {
    priority_queue<int, vector<int>, greater<int>> low;   // min-heap
    priority_queue<int> high;                             // max-heap
    for (int x : {5, 1, 3}) { low.push(x); high.push(x); }
    cout << low.top() << " "; low.pop();
    cout << low.top() << " " << high.top() << "\n";
}
import heapq


def library_heaps():
    """heapq is a min-heap on a plain list; for a max-heap, push the negatives."""
    low, high = [], []
    for x in (5, 1, 3):
        heapq.heappush(low, x)
        heapq.heappush(high, -x)
    first = heapq.heappop(low)
    print(first, low[0], -high[0])
// A min-heap by natural order; a comparator makes a max-heap.
static void libraryHeaps() {
    PriorityQueue<Integer> low = new PriorityQueue<>();
    PriorityQueue<Integer> high = new PriorityQueue<>(Comparator.reverseOrder());
    for (int x : new int[]{5, 1, 3}) { low.offer(x); high.offer(x); }
    int first = low.poll();
    System.out.println(first + " " + low.peek() + " " + high.peek());
}
// No heap is built in: the MinHeap above, and with (x, y) => x > y it keeps the largest on top.
function libraryHeaps() {
    const low = new MinHeap(), high = new MinHeap((x, y) => x > y);
    for (const x of [5, 1, 3]) { low.push(x); high.push(x); }
    const first = low.pop();
    console.log(first, low.a[0], high.a[0]);
}

Each printed 1 3 5: the smallest of 5, 1, 3; the smallest after popping it; and the largest from the max-heap.

Heap sort, and Last Stone Weight

Heapify, then take the top n times, and the values come out in order: heap sort, O(n log n). In place, it builds a max-heap and swaps the top to the end of the array, one value at a time. It isn't stable, and in practice your language's sort is faster, but it's how the heap guarantees its bound:

def heap_sort(a):
    """In place: a max-heap, then swap the top to the end and sift down what's left."""
    def sift_down(i, n):
        while True:
            l, r, m = 2 * i + 1, 2 * i + 2, i
            if l < n and a[l] > a[m]:
                m = l
            if r < n and a[r] > a[m]:
                m = r
            if m == i:
                return
            a[i], a[m] = a[m], a[i]
            i = m

    for i in range(len(a) // 2 - 1, -1, -1):
        sift_down(i, len(a))
    for end in range(len(a) - 1, 0, -1):
        a[0], a[end] = a[end], a[0]              # the biggest goes to the end
        sift_down(0, end)

It sorted [5, 2, 8, 1, 9, 3] into [1, 2, 3, 5, 8, 9].

Last Stone Weight smashes the two heaviest stones each turn, so it wants the largest on top. In Python, push the negatives:

import heapq


def last_stone_weight(stones):
    heap = [-s for s in stones]                 # heapq is a min-heap: negatives make it a max-heap
    heapq.heapify(heap)
    while len(heap) > 1:
        y, x = -heapq.heappop(heap), -heapq.heappop(heap)   # the two heaviest
        if y != x:
            heapq.heappush(heap, -(y - x))
    return -heap[0] if heap else 0

It returned 1 for the example [2, 7, 4, 1, 8, 1] and 1 for [1]. Each turn is two pops and at most one push, O(n log n) in all.

Spotting it, and when it breaks

The clues. "The smallest" or "the largest" asked again and again while values keep arriving or leaving; "the next" task, event or meeting; "repeatedly take the best two": a heap.

The near-misses. Values that never change: sort once (Sorting). Only one minimum, once: a single pass. The smallest or largest of a sliding window: a monotonic deque is O(1) per step (Monotonic Deque: the Window's Maximum). A floor or ceiling, not just the top: an ordered set (Balanced Trees and Ordered Sets).

When it breaks. A heap can't find or delete an arbitrary value fast: Java's remove(x) is O(n), and the usual fix is to skip stale entries when they reach the top. It knows nothing about order below the root, so it can't list its values sorted without popping them all.

Bugs that cost a submission

Sifting down toward the bigger child.

def sift_down(a, i):
    while True:
        l, r, m = 2 * i + 1, 2 * i + 2, i
        if l < len(a) and a[l] < a[m]:
            m = l
        if r < len(a) and a[r] > a[m]:           # picks the bigger child when both are smaller
            m = r
        if m == i:
            return
        a[i], a[m] = a[m], a[i]
        i = m

Popping [1, 2, 3, 4, 8, 5, 7] until empty gave 1 3 5 7 8 2 4; the answer is 1 2 3 4 5 7 8. Compare the right child with the left one's winner, a[r] < a[m], so the smaller child rises.

A comparator that overflows (Java).

static PriorityQueue<Integer> minHeap() {
    return new PriorityQueue<>((x, y) -> x - y);    // x - y overflows for values far apart
}

With −2,000,000,000 and 2,000,000,000 queued it polled 2000000000 first: x - y overflowed and flipped the sign. Use Integer.compare(x, y), or no comparator at all for natural order.

The default priority_queue as a min-heap (C++).

// meant as a min-heap
priority_queue<int> pq;

After pushing 5, 1 and 3, top() was 5, the largest. Declare priority_queue<int, vector<int>, greater<int>> for the smallest on top.

Sorting on every push (JavaScript).

function sortedPushes(vals) {
    const a = [];
    for (const x of vals) { a.push(x); a.sort((p, q) => p - q); }   // a sorted array, re-sorted on every push
    return a[0];
}

For 8,000 values it took 740.8 ms against 6.5 ms for the heap above, with the same answer. Even a sort that's quick on nearly sorted input moves the whole array each time.

Check yourself

Quiz

1 / 6

Multiple choice

In the min-heap array [1, 3, 2, 7, 4, 5], which index holds the parent of index 4?

Practice problems

In this order:

  1. Priority Queue via Binary Heap: push and pop by hand.
  2. Min-Heap & Max-Heap Implementation: the same sift with the order flipped.
  3. Validate Heap Property in Array: every parent against its children.
  4. Convert Min Heap to Max Heap: build bottom-up, in O(n).
  5. Last Stone Weight: a max-heap of stones.

Next, Top K and the Kth Element: a heap of size k for the best k values.