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 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.
- 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 because heap height is .
- Build-heap via bottom-up sift-down costs total, not — a subtle but important amortized bound.
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 : ``` 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 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 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 . Every core operation — inserting an element, removing the extreme element, or repairing a single violation — touches at most one root-to-leaf path, giving 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 because every index from to 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 , both of its subtrees are already valid heaps — an invariant argued formally below.
Complexity & correctness
Height and per-operation cost. A nearly complete binary tree on nodes has height : 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 work per level, so each costs in the worst case. `INSERT` and `EXTRACT-MAX` each do extra work plus one sift, so they are as well. `FIND-MAX` (peek at index 0) is .
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 iterations, at a leaf at the latest.
Correctness of build-heap. `BUILD-MAX-HEAP` processes indices — that is, in decreasing index order. The loop invariant is: at the start of the iteration processing index , every node with index greater than is the root of a valid max-heap. This holds initially (vacuously, and because all indices are leaves, each trivially a size-1 heap). When the loop reaches index , both `left(i)` and `right(i)` exceed , so by the invariant their subtrees are already valid heaps — exactly the precondition `SIFT-DOWN` needs. After the call, the subtree rooted at is also a valid heap, restoring the invariant with decremented. When the loop exits (after processing ), the entire array is a valid max-heap.
The build-heap bound. A naive bound charges each of the sift-down calls its worst case , giving — correct but not tight. The precise argument: a node at height (leaves have height 0) can sift down at most levels, and there are at most nodes at height in a heap of elements. Total work is bounded by
The series converges to (a standard arithmetico-geometric sum), so the whole expression is . 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 " beats the linear growth in "how far they can move," producing a linear, not linearithmic, total.
Space. A binary heap needs 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 , unlike a balanced BST's . Merging two heaps is 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, ). First, , so the loop runs for .
| Step | Subtree at before | Action | Array after | |
|---|---|---|---|---|
| start | — | — | initial array | `[4, 10, 3, 5, 1, 15, 8]` |
| 1 | 2 | root `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]` |
| 2 | 1 | root `A[1]=10`, children `A[3]=5`, `A[4]=1` | 10 already ≥ both children; no swap | `[4, 10, 15, 5, 1, 3, 8]` |
| 3 | 0 (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]` |
| 4 | 0 (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
| Pitfall | Why it happens | Fix |
|---|---|---|
| Off-by-one in child formulas | Mixing 0-indexed (`2i+1, 2i+2`) and 1-indexed (`2i, 2i+1`) conventions mid-implementation | Pick one convention and use it consistently for `parent`, `left`, `right` everywhere |
| Forgetting the `heap_size` bound in sift-down | Comparing against `A[left(i)]` without checking `left(i) < heap_size` reads past the logical heap into stale or out-of-bounds data | Always 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 element | Remember: the moved element is at the root, likely too small — it must move down, not up |
| Iterating build-heap forward instead of backward | Calling sift-down from `i = 0` upward violates the precondition that child subtrees are already valid heaps | Iterate `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 siblings | Never 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 type | Change 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
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.
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 worst case and strictly in-place 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 height, but they guarantee different things. A BST guarantees a global order — an in-order traversal yields every key in sorted order — which supports search for arbitrary keys. A heap only guarantees a local parent-versus-children relationship, so it can find the maximum (or minimum) in and remove it in , but searching for an arbitrary key takes 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 can sift down at most levels, and the number of nodes at height shrinks geometrically as grows (roughly nodes at height ). Summing (nodes at height ) × (max levels moved) over all gives , and that infinite series converges to a constant (2), so the whole sum is . 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 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 extractions the array is fully sorted in place, in total time with only 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 -th largest element for larger 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 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 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.