The heap

A tree in an array that always knows its smallest element - in O(log n).

22 min read

Start here: what this lesson is for

A heap is the answer to a question that sounds like it needs sorting but does not: what is the smallest thing right now?, asked repeatedly while new items keep arriving.

Sorting answers that, and charges you n log n to put everything in order when you only ever look at one end. A heap does exactly as much work as the question requires and no more - which makes it a good lesson in fitting the structure to the question rather than reaching for the familiar one.

It is also the most physically interesting structure in the course: a tree with no references at all, living inside a flat array, with arithmetic standing in for pointers.

Remember these
  • What you need first: chapter 8 (trees) and chapter 2 (arrays and memory).
  • What you will be able to do: use a priority queue correctly and know why iterating one is not sorted.
  • How long: about 22 minutes, plus two problems at the end.
Words you just learned
priority queue
- serve the highest-priority item first, rather than the oldest

Sorting is more than you needed

You want the smallest item, repeatedly, while new items keep arriving. Sorting gives you that - and charges n log n to put everything in order when you only ever look at one end.

A heap does exactly as much work as the question requires. It keeps one promise and no more: every node is smaller than its children. That's the heap property, and notice what it does not say - it says nothing about left versus right, and nothing about ordering between siblings.

Because the promise is weak, it's cheap to maintain. And because it's applied at every level, the smallest element in the entire structure has nowhere to hide: it must be at the root. Reading the minimum is O(1).

Imagine it like this: A company where every manager earns less than their reports. Nobody is ranked overall, but the lowest-paid person is guaranteed to be the boss - so finding them takes no searching at all.

Words you just learned
heap property
- every node is smaller than (or equal to) both of its children
min-heap
- smallest at the root; a max-heap is the mirror image

A tree with no references

Here's the part that makes heaps fast in a way Big-O doesn't show. A heap is a complete binary tree - every level full except possibly the last, which fills left to right - and a complete tree can be stored in a plain array, with no node objects and no references at all.

Index arithmetic replaces the pointers. The children of index i live at 2i + 1 and 2i + 2; the parent lives at (i - 1) / 2. Walking the tree becomes multiplication.

Chapter 6 warned that scattered nodes cost cache misses. A heap has none of that - it is one contiguous block, so the constant factor is excellent on top of the good asymptotics. It is a tree with a linked list's flexibility and an array's memory behaviour.

HeapLayout.java
java
int parent(int i) { return (i - 1) / 2; }
int left(int i)   { return 2 * i + 1; }
int right(int i)  { return 2 * i + 2; }

//        1(0)
//       /    \
//    3(1)    2(2)
//    /  \    /
//  7(3) 4(4) 5(5)
What it prints
array:  [1, 3, 2, 7, 4, 5]
index:   0  1  2  3  4  5

left(1)  = 3  -> value 7
right(1) = 4  -> value 4
parent(4)= 1  -> value 3

no Node objects, no references, one contiguous block
The tree drawing and the array are the same thing. Nothing stores the shape, because completeness means the shape is implied by the length.
Words you just learned
complete tree
- every level full except the last, which fills left to right

Sift up, sift down

Two operations maintain the promise, and both are just 'swap with a neighbour until the promise holds again'.

Insert: put the new value at the end of the array - the only spot that keeps the tree complete - then sift up, swapping with its parent while it's smaller. It rises to its level and stops.

Remove the minimum: the root leaves, and to keep the tree complete you move the last element into the root. It's almost certainly too big, so sift down, swapping with its smaller child until the promise holds.

Both walk one root-to-leaf path, and a complete tree's height is log n. That's the O(log n), and the swaps are adjacent array writes, which is why heaps are quick in practice too.

Heap.java
java
void insert(int value) {
    heap.add(value);                       // end of the array keeps it complete
    int i = heap.size() - 1;
    while (i > 0 && heap.get(i) < heap.get(parent(i))) {
        swap(i, parent(i));                // sift up
        i = parent(i);
    }
}

int removeMin() {
    int min = heap.get(0);
    heap.set(0, heap.remove(heap.size() - 1));   // last element to the root
    int i = 0;
    while (true) {                          // sift down
        int smallest = i;
        if (left(i)  < heap.size() && heap.get(left(i))  < heap.get(smallest)) smallest = left(i);
        if (right(i) < heap.size() && heap.get(right(i)) < heap.get(smallest)) smallest = right(i);
        if (smallest == i) return min;      // promise restored
        swap(i, smallest);
        i = smallest;
    }
}
What it prints
insert 1 into [3, 4, 8, 9, 5]
  append   [3,4,8,9,5,1]
  sift up  1 < 8  -> [3,4,1,9,5,8]
  sift up  1 < 3  -> [1,4,3,9,5,8]     2 swaps, height 3

removeMin -> 1
  move last to root  [8,4,3,9,5]
  sift down 8 > 3    [3,4,8,9,5]       promise holds
Sift down must swap with the smaller of the two children. Swap with the larger and you have simply moved the problem down while breaking the promise on the other side.
Java gives you this

PriorityQueue is a binary min-heap. offer inserts, poll removes the minimum, peek reads it in O(1). For a max-heap, pass a reversed comparator - new PriorityQueue<>(Comparator.reverseOrder()). One warning that catches everyone: iterating a PriorityQueue does not give sorted order, because the heap was never fully sorted. Only repeated poll does.

Remember these
  • Heap property: every node smaller than its children - nothing more
  • Complete tree stored in an array; children at 2i+1, 2i+2
  • peek O(1), insert and removeMin O(log n)
  • Sift up on insert, sift down on remove - swap with the SMALLER child
  • Java: PriorityQueue. Iteration order is not sorted order
Words you just learned
sift up / sift down
- restoring the heap property by swapping along one path
priority queue
- the abstract idea - serve the highest priority first; a heap is how it's built

Building a heap from scratch, cheaply

Suppose you already have an array and want it to become a heap. The obvious way is to insert every element one at a time, which costs O(log n) each and O(n log n) overall - the same as sorting, which rather defeats the point.

There is a better way, and it is delightfully counter-intuitive. Start at the last non-leaf node and sift down, working backwards to the root. That is O(n) - building a heap is cheaper than sorting.

The reason is that most nodes are near the bottom, where sifting down has almost nowhere to go. Half the nodes are leaves and need no work at all; a quarter can fall at most one level; only the root can fall the full log n. Sum that series and it converges to about 2n.

This is why heapsort exists: build a heap in O(n), then remove the minimum n times at O(log n) each. Guaranteed O(n log n), in place, with no bad input - the guarantee quicksort could not give you in chapter 4.

Heapify.java
java
void buildHeap(int[] a) {
    // start at the last node that HAS a child and work backwards
    for (int i = a.length / 2 - 1; i >= 0; i--) {
        siftDown(a, i, a.length);
    }
}
What it prints
n = 1,000,000

insert one at a time   ~20,000,000 operations   O(n log n)
buildHeap (sift down)   ~2,000,000 operations   O(n)

why: how far each node can actually fall
  500,000 leaves        0 levels   ->        0
  250,000 nodes         1 level    ->  250,000
  125,000 nodes         2 levels   ->  250,000
  ...
        1 root         20 levels   ->       20
                                     ~1,000,000 total
Read the breakdown. Half the nodes are leaves and cost nothing, and the expensive nodes are vanishingly rare. That is why the total is linear rather than n log n.
Words you just learned
heapify
- turning an arbitrary array into a heap in place
heapsort
- build a heap, then repeatedly remove the minimum - O(n log n), in place, guaranteed

Recap: partial order, on purpose

A heap keeps exactly one promise - every node is smaller than its children - and refuses to pay for anything more. That refusal is the entire design.

Because the promise applies at every level, the minimum has nowhere to hide: it must be at the root, so peeking is O(1). Because the promise is weak, restoring it after a change means walking one root-to-leaf path, so insert and remove are O(log n).

Because a heap is a complete tree, its shape is implied by its length, so it lives in a flat array with children at 2i+1 and 2i+2 - no node objects, no references, and excellent cache behaviour on top of good asymptotics.

The one thing to carry away as a warning: iterating a PriorityQueue does not give sorted order. The heap was never fully sorted. Only repeated poll() gives you order, and this catches nearly everyone once.

Remember these
  • Heap property: every node smaller than its children - nothing more
  • peek O(1), insert and removeMin O(log n), build O(n)
  • Complete tree in a flat array: children at 2i+1 and 2i+2
  • Sift down must swap with the SMALLER child
  • Iterating a PriorityQueue is not sorted - only polling is

Your turn

Reading is not learning. Open each one in NeonFlow and watch your own code run, step by step.

Problem 1

Prove iteration is not sorted

Add several values to a PriorityQueue, then print it two ways: by iterating, and by polling until empty. Predict both outputs first. The difference is the single most common heap misunderstanding.

Starter
java
import java.util.*;

public class NotSorted {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (int v : new int[]{5, 1, 8, 3, 9, 2}) pq.offer(v);

        System.out.println("iterating: " + pq);   // the raw heap ARRAY

        // TODO: poll until empty, collecting into a list, and print that
        List<Integer> polled = new ArrayList<>();

        System.out.println("polling:   " + polled);
    }
}
Problem 2

Top-K with a min-heap

Keep the 3 largest values from a stream using a heap that never holds more than 3 items. The counter-intuitive part: to track the largest values you need a min-heap, because its root is the weakest survivor - the one a newcomer must beat.

Starter
java
import java.util.*;

public class TopK {
    static int[] topK(int[] values, int k) {
        // TODO: a MIN-heap (natural ordering) holding at most k items
        PriorityQueue<Integer> best = new PriorityQueue<>();

        for (int v : values) {
            // TODO: offer v, then if the heap is bigger than k, poll off
            //       the smallest - it can never be in the top k again
        }

        int[] out = new int[best.size()];
        for (int i = 0; i < out.length; i++) out[i] = best.poll();
        return out;
    }

    public static void main(String[] args) {
        System.out.println(Arrays.toString(topK(new int[]{5,1,9,3,14,7}, 3)));
        // expect the three largest: 7, 9, 14 (in ascending order)
    }
}