Binary Heaps

Imagine a hospital emergency room. It must always treat the most critical patient next. This happens even as new patients arrive continuously. A sorted waiting list would need expensive re-sorting on every arrival. What is needed is a structure that always exposes the "most urgent" item instantly. Insertion and removal must both stay cheap. The binary heap solves exactly this problem. It does so with a strikingly simple trick: storing a tree inside a plain array, with no pointers at all. Binary heaps power priority queues. These sit underneath Dijkstra's shortest-path algorithm, event-driven simulations, operating system schedulers, and the Heapsort algorithm. Their appeal is a combination of two properties that rarely coexist so cleanly. These are logarithmic-time insertion and removal of the extreme element, and constant extra space beyond the array itself. Unlike a balanced binary search tree, a heap needs no rotations, no parent pointers, and no rebalancing logic. The array indices themselves encode the tree shape. The big picture is this: a heap is a nearly complete binary tree satisfying an ordering constraint (the heap property). That tree is laid out implicitly in an array using simple arithmetic on indices. Two symmetric operations, sift-up (bubble up) and sift-down (bubble down), restore the heap property after a single change. This happens near a leaf or near the root, respectively. Repeated sift-down operations, applied bottom-up over an unordered array, build an entire heap in linear time. This is a result that looks like it should cost O(nlog⁡n)O(n \log n) but provably does not. This article builds the data structure from first principles. It covers how the array encodes the tree, how each operation preserves the heap invariant, and why the build-heap procedure is asymptotically optimal. It closes with the trace of a real build, a catalogue of the bugs that trip up first implementations. It also covers the historical route from Williams' 1964 paper to the ubiquitous priority queues of today.

Key takeaways
  • A binary heap is a nearly complete binary tree stored implicitly in an array — no explicit pointers needed.
  • The heap property (max-heap: parent ≥ children) makes the root always the maximum (or minimum) element.
  • `parent(i) = ⌊(i-1)/2⌋`, `left(i) = 2i+1`, `right(i) = 2i+2` (0-indexed) give the tree shape from array indices.
  • Sift-up restores order after inserting at the end; sift-down restores order after replacing the root.
  • Both operations run in O(log⁡n)O(\log n) because heap height is ⌊log⁡2n⌋\lfloor \log_2 n \rfloor.
  • Build-heap via bottom-up sift-down costs O(n)O(n) total, not O(nlog⁡n)O(n \log n) — a subtle but important amortized bound.
The core insight

A binary heap encodes a balanced tree's shape as pure arithmetic on array indices, trading pointers for index math to get logarithmic insert/extract-max with constant overhead.

The idea

A binary heap is a data structure representing a nearly complete binary tree — every level is full except possibly the last, which is filled left to right — stored in a plain array with no explicit child or parent links. The tree structure is implicit: the position of an element in the array alone determines its parent and children via arithmetic. Using 0-based indexing, for a node at index ii: ``` parent(i) = ⌊(i - 1) / 2⌋ left(i) = 2i + 1 right(i) = 2i + 2 ``` This works because a nearly complete tree, read level by level (a level-order traversal), produces exactly the sequence of array slots 0,1,2,…,n−10, 1, 2, \ldots, n-1 with no gaps — the defining structural guarantee that makes the arithmetic valid. The second ingredient is the heap property (also called the heap invariant). In a max-heap, every node's key is greater than or equal to its children's keys:

A[parent(i)]≥A[i]for all i>0.A[\text{parent}(i)] \ge A[i] \quad \text{for all } i > 0.

A min-heap reverses the inequality. The heap property is local — it only constrains parent–child pairs — yet it has a powerful global consequence: the maximum (min-heap: minimum) element of the entire tree always sits at the root, index 0. Note that a heap is not a sorted structure; siblings and distant relatives have no ordering constraint between them at all, which is precisely what makes heap operations cheaper than full sorting. Because the tree is balanced by construction (nearly complete), it has height h=⌊log⁡2n⌋h = \lfloor \log_2 n \rfloor. Every core operation — inserting an element, removing the extreme element, or repairing a single violation — touches at most one root-to-leaf path, giving O(log⁡n)O(\log n) per operation. This connects heaps to Balanced Binary Search Trees in spirit (both exploit logarithmic height) but heaps achieve it with none of the rebalancing machinery, because they only need a much weaker ordering guarantee.

How it works

Sift-up (used after appending a new element at the end of the array) walks the new element toward the root, swapping with its parent as long as the heap property is violated: ``` SIFT-UP(A, i): while i > 0 and A[parent(i)] < A[i]: swap(A[parent(i)], A[i]) i = parent(i) ```

Sift-down (used after replacing the root, typically during extraction) walks an element toward the leaves, swapping with its larger child until no violation remains: ``` SIFT-DOWN(A, i, heap_size): while true: l = left(i) r = right(i) largest = i if l < heap_size and A[l] > A[largest]: largest = l if r < heap_size and A[r] > A[largest]: largest = r if largest == i: break swap(A[i], A[largest]) i = largest ```

Insert appends the new key at the end (preserving the shape property automatically, since the next open array slot is always the correct next tree position) and then sifts it up: ``` INSERT(A, heap_size, key): A[heap_size] = key heap_size = heap_size + 1 SIFT-UP(A, heap_size - 1) ```

Extract-max removes the root, patches the hole by moving the last element into position 0 (again preserving shape), and sifts down to fix the single violation this creates: ``` EXTRACT-MAX(A, heap_size): if heap_size == 0: error "heap is empty" max = A[0] A[0] = A[heap_size - 1] heap_size = heap_size - 1 SIFT-DOWN(A, 0, heap_size) return max ```

Build-heap converts an arbitrary array into a valid heap by calling sift-down on every internal node, working from the last internal node up to the root — critically, in reverse level order, not forward: ``` BUILD-MAX-HEAP(A, n): for i = ⌊n/2⌋ - 1 downto 0: SIFT-DOWN(A, i, n) ``` The loop starts at ⌊n/2⌋−1\lfloor n/2 \rfloor - 1 because every index from ⌊n/2⌋\lfloor n/2 \rfloor to n−1n-1 is a leaf (it has no children within the array bounds), and leaves trivially satisfy the heap property alone. Processing bottom-up ensures that by the time `SIFT-DOWN` is called on node ii, both of its subtrees are already valid heaps — an invariant argued formally below.

Figure
A max-heap and its implicit array layout
Max-heap: tree view (top) and array view (bottom) 16 14 10 8 7 9 3 2 4 i=0 i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8 16 14 10 8 7 9 3 2 4 0 1 2 3 4 5 6 7 8 left(i)=2i+1, right(i)=2i+2, parent(i)=⌊(i-1)/2⌋ — same 9 values, two views

Complexity & correctness

Height and per-operation cost. A nearly complete binary tree on nn nodes has height h=⌊log⁡2n⌋h = \lfloor \log_2 n \rfloor: each level except possibly the last is full, so the tree cannot be shallower without violating completeness, and it cannot be deeper without more than one incomplete level. Both `SIFT-UP` and `SIFT-DOWN` follow a single root-to-leaf (or leaf-to-root) path, doing O(1)O(1) work per level, so each costs O(log⁡n)O(\log n) in the worst case. `INSERT` and `EXTRACT-MAX` each do O(1)O(1) extra work plus one sift, so they are O(log⁡n)O(\log n) as well. `FIND-MAX` (peek at index 0) is O(1)O(1).

Correctness of sift-down (invariant argument). Consider `SIFT-DOWN(A, i, n)` called when both subtrees rooted at `left(i)` and `right(i)` are already valid max-heaps, but `A[i]` may be smaller than one of its children. The loop's invariant is: at the top of each iteration, the subtree rooted at the current `i` would be a valid heap if `A[i]` were replaced by any value at least as large as `max(A[left(i)], A[right(i)])`. Each iteration identifies the largest of `A[i]` and its children; if `A[i]` is already largest, the local heap property holds and, since both subtrees are already valid, the whole subtree at `i` is valid — the loop correctly terminates. Otherwise it swaps the offending value down into the larger child's slot, which fixes the violation at `i` (the new `A[i]` is the true maximum of the three) while pushing the smaller original root value one level down, where the invariant is re-established for the next iteration. Since each iteration strictly decreases the number of levels remaining below `i`, the loop terminates within hh iterations, at a leaf at the latest.

Correctness of build-heap. `BUILD-MAX-HEAP` processes indices ⌊n/2⌋−1,…,1,0\lfloor n/2 \rfloor - 1, \ldots, 1, 0 — that is, in decreasing index order. The loop invariant is: at the start of the iteration processing index ii, every node with index greater than ii is the root of a valid max-heap. This holds initially (vacuously, and because all indices ≥⌊n/2⌋\ge \lfloor n/2 \rfloor are leaves, each trivially a size-1 heap). When the loop reaches index ii, both `left(i)` and `right(i)` exceed ii, so by the invariant their subtrees are already valid heaps — exactly the precondition `SIFT-DOWN` needs. After the call, the subtree rooted at ii is also a valid heap, restoring the invariant with ii decremented. When the loop exits (after processing i=0i = 0), the entire array is a valid max-heap.

The O(n)O(n) build-heap bound. A naive bound charges each of the ≈n/2\approx n/2 sift-down calls its worst case O(log⁡n)O(\log n), giving O(nlog⁡n)O(n \log n) — correct but not tight. The precise argument: a node at height kk (leaves have height 0) can sift down at most kk levels, and there are at most ⌈n/2k+1⌉\lceil n / 2^{k+1} \rceil nodes at height kk in a heap of nn elements. Total work is bounded by

∑k=0⌊log⁡2n⌋⌈n2k+1⌉⋅O(k)  =  O ⁣(n∑k=0∞k2k).\sum_{k=0}^{\lfloor \log_2 n \rfloor} \left\lceil \frac{n}{2^{k+1}} \right\rceil \cdot O(k) \;=\; O\!\left(n \sum_{k=0}^{\infty} \frac{k}{2^k}\right).

The series ∑k=0∞k/2k\sum_{k=0}^{\infty} k / 2^k converges to 22 (a standard arithmetico-geometric sum), so the whole expression is O(n)O(n). Intuitively: most nodes live near the bottom of the tree (half the nodes are leaves, a quarter are one level up, and so on), and nodes near the bottom can only sift down a few levels — the geometric decay in "how many nodes at height kk" beats the linear growth in "how far they can move," producing a linear, not linearithmic, total.

Space. A binary heap needs O(1)O(1) auxiliary space beyond the array — no pointers, no per-node overhead — which is part of why heaps outperform pointer-based priority queue implementations in practice despite equal asymptotic time bounds. This is the same array, in place; `BUILD-MAX-HEAP` and `EXTRACT-MAX` both operate destructively on the input array, which is exactly what Heapsort exploits to sort in place.

Honest tradeoffs. A heap gives fast access only to the extreme element — searching for an arbitrary key is O(n)O(n), unlike a balanced BST's O(log⁡n)O(\log n). Merging two heaps is O(n)O(n) for the standard array-based heap (specialized structures like binomial or Fibonacci heaps trade simplicity for faster merge/decrease-key). And a heap provides no in-order traversal — it is not a substitute for a sorted array when the full order matters.

Worked example

Build a max-heap from the unsorted array `A = [4, 10, 3, 5, 1, 15, 8]` (indices 0–6, n=7n = 7). First, ⌊n/2⌋−1=⌊7/2⌋−1=2\lfloor n/2 \rfloor - 1 = \lfloor 7/2 \rfloor - 1 = 2, so the loop runs for i=2,1,0i = 2, 1, 0.

StepiiSubtree at ii beforeActionArray after
start——initial array`[4, 10, 3, 5, 1, 15, 8]`
12root `A[2]=3`, children `A[5]=15`, `A[6]=8`largest child is 15 at index 5; swap `A[2]↔A[5]`; index 5 is a leaf, stop`[4, 10, 15, 5, 1, 3, 8]`
21root `A[1]=10`, children `A[3]=5`, `A[4]=1`10 already ≥ both children; no swap`[4, 10, 15, 5, 1, 3, 8]`
30 (pass a)root `A[0]=4`, children `A[1]=10`, `A[2]=15`largest is 15 at index 2; swap `A[0]↔A[2]``[15, 10, 4, 5, 1, 3, 8]`
40 (pass b)now at index 2: `A[2]=4`, children `A[5]=3`, `A[6]=8`largest is 8 at index 6; swap `A[2]↔A[6]`; index 6 is a leaf, stop`[15, 10, 8, 5, 1, 3, 4]`

The final array `[15, 10, 8, 5, 1, 3, 4]` is a valid max-heap: check every parent–child pair — `15 ≥ 10`, `15 ≥ 8` (root vs. children at 1, 2); `10 ≥ 5`, `10 ≥ 1` (node 1 vs. children at 3, 4); `8 ≥ 3`, `8 ≥ 4` (node 2 vs. children at 5, 6). All hold, confirming the invariant argued above. Now trace `EXTRACT-MAX` on this heap: 1. `max = A[0] = 15`. 2. Move last element to root: `A[0] = A[6] = 4`; shrink `heap_size` from 7 to 6. Array (logical heap): `[4, 10, 8, 5, 1, 3]`. 3. `SIFT-DOWN(A, 0, 6)`: root `A[0]=4`, children `A[1]=10`, `A[2]=8`; largest is 10 at index 1; swap → `[10, 4, 8, 5, 1, 3]`. 4. Continue at `i=1`: `A[1]=4`, children `A[3]=5`, `A[4]=1`; largest is 5 at index 3; swap → `[10, 5, 8, 4, 1, 3]`. 5. Continue at `i=3`: index 3 has no children within `heap_size=6` (`left(3)=7 ≥ 6`); loop stops. Result: `max = 15` is returned, and the remaining heap `[10, 5, 8, 4, 1, 3]` is valid — a second extraction would correctly return `10`.

Common pitfalls & traps

PitfallWhy it happensFix
Off-by-one in child formulasMixing 0-indexed (`2i+1, 2i+2`) and 1-indexed (`2i, 2i+1`) conventions mid-implementationPick one convention and use it consistently for `parent`, `left`, `right` everywhere
Forgetting the `heap_size` bound in sift-downComparing against `A[left(i)]` without checking `left(i) < heap_size` reads past the logical heap into stale or out-of-bounds dataAlways guard both child accesses with an explicit bounds check against the current heap size
Sifting the wrong direction after `extract-max`Calling `SIFT-UP` instead of `SIFT-DOWN` on the relocated last elementRemember: the moved element is at the root, likely too small — it must move down, not up
Iterating build-heap forward instead of backwardCalling sift-down from `i = 0` upward violates the precondition that child subtrees are already valid heapsIterate `i` from `⌊n/2⌋ - 1` down to `0`, so subtrees are always processed before their parents
Confusing "heap" with "sorted array"The heap property only orders parents against children, not siblingsNever assume in-order traversal of the array gives sorted output; extraction is required
Comparator sign error (min vs. max heap)Flipping `>` to `<` in only one of sift-up/sift-down when switching heap typeChange the comparison consistently in every comparison site, and re-derive both operations together

Pioneers & history

The binary heap was introduced as the backbone of an elegant in-place sorting algorithm, but its real legacy is the priority queue abstraction it makes efficient. J. W. J. Williams first described the array-based heap and Heapsort in 1964, in a short Communications of the ACM algorithm note — remarkably, in the same year that Robert Floyd published an improved, more efficient version of the build-heap procedure, the same bottom-up construction taught here. The heap concept was then generalized far beyond arrays: binomial heaps, Fibonacci heaps, and pairing heaps each relax the shape or invariant to optimize different operation mixes, feeding directly into faster implementations of Dijkstra's and Prim's graph algorithms.

Origins · 1 minJ. W. J. Williams and how this was discovered
JW
J. W. J. Williams
20th century · British computer scientist who introduced the heap data structure and Heapsort in a 1964 ACM algorithm

Williams' original note was brief — a few columns describing "Algorithm 232" — yet it introduced both the implicit array-tree encoding and the sift-down repair operation that remain, essentially unchanged, in every textbook treatment of heaps today.

History
1964
— J. W. J. Williams publishes Heapsort using the array-based binary heap.
1964
— Robert Floyd publishes the linear-time bottom-up build-heap procedure.
1978
— Jean Vuillemin introduces binomial heaps, generalizing the merge operation.
1984
— Michael Fredman and Robert Tarjan introduce Fibonacci heaps for faster decrease-key.
1987
— Fredman, Sedgewick, Sleator, and Tarjan introduce pairing heaps as a practical alternative.

Where this matters

Priority queues built on binary heaps are the default choice inside Dijkstra's Algorithm and Prim's minimum spanning tree algorithm, where the "extract the closest unvisited vertex" step is exactly `EXTRACT-MAX` (or its min-heap twin) repeated across the graph. Operating system schedulers use heap-based priority queues to pick the next process or timer event to run. Discrete-event simulators — used in network modeling, manufacturing systems, and game engines — rely on a heap keyed by event time to always process the next event in chronological order. Standard library priority queues (C++'s `std::priority_queue`, Java's `PriorityQueue`, Python's `heapq`) are direct implementations of the array-based binary heap described here. And Heapsort itself, while rarely the fastest sort in practice compared to well-tuned quicksort or mergesort variants, remains valuable whenever a guaranteed O(nlog⁡n)O(n \log n) worst case and strictly in-place O(1)O(1) extra space are both required simultaneously — a combination few other comparison sorts offer. Understanding heaps also clarifies the tradeoffs behind more advanced structures like Fibonacci Heaps, which sacrifice heap simplicity for amortized speedups on decrease-key-heavy workloads.

Ask the tutor: Can you show me how a min-heap's sift-up and sift-down pseudocode differ from the max-heap versions given here?

What is the difference between a heap and a balanced binary search tree?

Both structures have O(log⁡n)O(\log n) height, but they guarantee different things. A BST guarantees a global order — an in-order traversal yields every key in sorted order — which supports O(log⁡n)O(\log n) search for arbitrary keys. A heap only guarantees a local parent-versus-children relationship, so it can find the maximum (or minimum) in O(1)O(1) and remove it in O(log⁡n)O(\log n), but searching for an arbitrary key takes O(n)O(n) because no global order constrains where a key can sit. Heaps also need no rebalancing logic (no rotations), because the shape is always kept perfectly balanced by the array-append/array-shrink insert and delete operations, at the cost of only supporting extreme-element queries.

Why is build-heap O(n) instead of O(n log n) if it calls an O(log n) operation n/2 times?

The naive bound multiplies the number of calls by the worst-case cost per call, but that overcounts: most calls are cheap. A node at height kk can sift down at most kk levels, and the number of nodes at height kk shrinks geometrically as kk grows (roughly n/2k+1n/2^{k+1} nodes at height kk). Summing (nodes at height kk) × (max levels moved) over all kk gives n∑kk/2kn \sum_k k/2^k, and that infinite series converges to a constant (2), so the whole sum is O(n)O(n). The key insight is that the many cheap calls near the leaves dominate the count, while the few expensive calls near the root are rare.

Can a binary heap be implemented with explicit tree nodes and pointers instead of an array?

Yes, but it is almost never done in practice, because it loses the main advantage of the implicit encoding. With pointers, inserting the next element in level order requires tracking where the next open slot is, which itself needs extra bookkeeping (sometimes a counter in binary representing the path to the next free leaf). The array version gets this for free: the next open slot is always `A[heap_size]`, and parent/child relationships are pure arithmetic with no pointer-chasing or extra memory for links. The array form also has excellent cache locality since related nodes are numerically close in memory.

What happens if you call EXTRACT-MAX on an empty heap?

This is an error condition that must be checked explicitly before touching the array. If `heap_size` is 0, there is no root element to remove, and blindly reading `A[0]` either returns garbage from a previous configuration or reads out of bounds entirely. A correct implementation checks `heap_size == 0` first and raises an explicit error or returns a sentinel value, exactly as shown in the `EXTRACT-MAX` pseudocode. This mirrors the general discipline of checking loop invariants and preconditions before applying an operation that assumes a nonempty structure.

How does a binary heap relate to Heapsort?

Heapsort uses `BUILD-MAX-HEAP` to turn the input array into a heap in O(n)O(n) time, then repeatedly calls a variant of `EXTRACT-MAX` — swapping the root with the last live element and sifting down over a shrinking heap region — placing the maximum at the end of the array each time. After n−1n-1 extractions the array is fully sorted in place, in O(nlog⁡n)O(n \log n) total time with only O(1)O(1) auxiliary space. This is the same array and the same two operations described in this article; Heapsort is simply their systematic repeated application until the heap is empty.

Why does a max-heap not give you the second-largest element for free at index 1 or 2?

Because the heap property only orders each parent against its own children, not across the two subtrees. The second-largest overall element must be one of the root's two children (nothing deeper can beat both children on its own path to the root), so it is guaranteed to be at index 1 or 2 — but you cannot tell which one without comparing `A[1]` and `A[2]` directly, and neither position is fixed in general. Finding the kk-th largest element for larger kk requires either repeated extraction or an auxiliary selection structure; a heap by itself only makes rank-1 queries free.

Is a binary heap always the best priority queue implementation to use?

Not always. Array-based binary heaps are excellent when operations are "insert" and "extract-min/max," but they are O(n)O(n) for merging two heaps and for arbitrary "decrease-key" operations unless you also maintain a separate index into the array. Structures like Fibonacci Heaps offer amortized O(1)O(1) insert and decrease-key, which matters for algorithms like Dijkstra's on dense graphs, at the cost of significantly more implementation complexity and worse constant factors in practice. For most applications, the simplicity, cache-friendliness, and strong worst-case guarantees of the array-based binary heap make it the default choice.

Where this leads