Heaps and Heapsort
A binary heap is a tree we store flat in an array, with index arithmetic standing in for pointers. We build the max-heap property bottom-up in time, sort in place in by repeatedly extracting the maximum, and reuse the same structure to implement a priority queue.
╌╌╌╌
Quicksort is fast but has a quadratic worst case; mergesort guarantees but needs scratch space. achieves both: a worst-case bound and sorting in place, using only a constant amount of extra memory. It rests on a data structure, the binary heap, which doubles as an efficient priority queue and so is worth knowing for its own sake well beyond sorting.
The heap as a tree we never build
A (binary) max-heap is a complete binary tree that obeys one local rule.1
Complete
means the tree is filled level by level, top to bottom and left to
right, with no gaps until possibly the last level. That rigidity is what lets us
discard the tree entirely and store it as a flat array: there is exactly one
shape for a complete tree on nodes, so position in the array is position
in the tree.
We store the heap in an array in level order: the root at , then its two children, then their four children, and so on. With -based indexing the navigation is pure arithmetic, no pointers required:
Doubling an index walks down to a left child; halving walks back up to a parent. On a machine these are single shift operations, which is part of why heaps are fast in practice. Here is a small max-heap shown both as a tree and as the array that actually lives in memory:
Read level by level, the array is
The blue numbers below trace the correspondence: each tree node lives at array slot , so doubling the index (, ) steps down to a child and halving () steps back up to the parent.
Check the index rule on : its children are at and , both smaller, and its parent is at , larger. The largest key always sits at the root, . A heap on nodes is complete, so its height, the number of edges on the longest root-to-leaf path, is . Every operation below costs at most one such trip up or down the tree.
Two structural facts fall straight out of the indexing and get used constantly.
First, node is a leaf exactly when ,
that is when ; so the leaves are precisely
, and a heap is always at least half leaves
( of them). Second, the subtree rooted at any node is itself a heap,
so every claim we prove about the root
applies recursively everywhere.
A heap is not a sorted array: it is far weaker, ordering only along ancestor-descendant paths, with no relation between siblings or cousins. That weakness is what makes a heap cheap to maintain.
Sift-down: restoring the property at one node
The single primitive every heap operation rests on is . It assumes the subtrees rooted at the two children of are already max-heaps, but itself may be smaller than a child and thus violate the property. It repairs the violation by moving down to its correct level, a motion usually called sift-down.2
- 1
- 2
- 3if and then
- 4
- 5else
- 6
- 7if and then
- 8
- 9if then
- 10exchange withbigger child moves up
- 11callfix the disturbed subtree
We compare against both children, find the largest of the three, and, if a child wins, swap it up and recurse into the subtree that just received the smaller key. The violation moves strictly down one level each step, so the recursion can run no deeper than the tree is tall: time, extra space (or stack, trivially made iterative). We track the logical length of the heap in a field , which lets the array hold heap and non-heap regions side by side, an arrangement central to heapsort below.
A full trace with numbers. Take and call ; both subtrees under node are already heaps, but is smaller than its children.
- At : children are and ; the largest of is at index , so exchange and recurse on .
- At : children are and ; the largest of is at index , so exchange and recurse on .
- At : , so node is a leaf and the recursion stops.
Two swaps, and the violation is gone. Each swap moved the small key down one level, which is why the total work is bounded by the height.
Building a heap bottom-up
To turn an arbitrary array into a heap we call at every internal node, but in the right order. The leaves are already valid one-element heaps, so we start just above them and work upward to the root. Processing a node only after its children are heaps satisfies the precondition demands.
- 1
- 2for downto do
- 3callchildren already heaps
On nodes the leaves are (already heaps, so we skip them), and we sift down the internal nodes in that decreasing order, so that every node is processed only after both of its children are.
The loop invariant makes the correctness immediate.
Initialization: nodes are leaves, hence trivial heaps. Maintenance: the children of are numbered higher than , so by the invariant they head max-heaps, precisely what needs, and it extends the property to . Termination: when , node (and all others) roots a max-heap.
A full build, step by step
Run on the unsorted array , with and first internal node .
- : has one child, . Already the larger; no swap.
- : versus children , . Swap with ; the recursion hits a leaf and stops.
- : versus , . Swap with ; leaf, stop.
- : versus , . Swap with ; then at node , , so swap again; leaf, stop. The key sank two levels.
- : versus and . Swap with ; at node , versus : swap with ; at node , versus : swap with ; leaf, stop. Three levels, the full height.
The result reproduces the heap from the start of the lesson, . Late iterations do more work per call, but there are geometrically fewer of them; the next section shows the total is linear.
Why it is , not
The easy bound is immediate: there are calls to , each costing , for . That is correct but loose, and the looseness matters. Most nodes are near the bottom of the tree, where has almost nothing to do.
A heap of nodes has at most nodes at height , and a sift-down from height costs . Summing the real work over all heights,
The series converges to , using the standard identity at . The infinite sum is a constant, so the whole bound collapses to .
The intuition behind the algebra: half the nodes are leaves (zero work), a quarter sit one level up (at most one swap each), an eighth two levels up, and so on. Cost per node falls geometrically as the number of nodes at that level rises geometrically, and the two effects cancel to leave a linear total.3 Building a heap is asymptotically negligible compared to the sort that follows.
Heapsort
A max-heap keeps the largest element at the root, . To sort, we repeatedly move that maximum to where it belongs, the back of the array, then shrink the heap and repair it.
- 1call
- 2for downto do
- 3exchange withmax to its final slot
- 4evict from the heap
- 5callrestore the shrunken heap
The array splits into two regions: a heap at the front, , and a growing sorted suffix at the back. Each iteration swaps the heap's maximum into the slot just before the sorted suffix, drops the heap size by one, and runs a single sift-down from the root to re-establish the max-heap property on the smaller heap. After iterations the heap is a single element, necessarily the global minimum, and is sorted ascending.
The invariant holds initially because makes all of a heap and the suffix is empty. Each iteration maintains it: is the maximum of the heap region, hence no larger than anything already in the suffix and no smaller than anything left in the heap, so swapping it into the slot just before the suffix extends the sorted region by one correct element; shrinking the heap and sifting the displaced key down restores the heap half of the invariant. At termination the heap region is the single smallest element sitting in , so the whole array is sorted.
Watching the first few extractions on our running heap makes the two-region structure visible:
Row by row: swapping with the last leaf puts at the root, and the sift-down walks it back to the bottom (, then , then ), producing ; the next extraction swaps with and sifts again. Every extraction pays one root-to-leaf trip, , and there is no lucky input: after the swap the root holds a key that came from a leaf, almost always small, so the sift-down nearly always runs the full height.
auxiliary space beyond the array) but not stable: the swaps scatter equal keys. Compared to its peers it lacks quicksort's cache-friendliness, since the parent/child jumps roam across memory, which is why quicksort usually wins in practice despite the worse worst case. Heapsort's niche is the guaranteed ceiling with no extra memory, exactly the property introsort borrows as a fallback when quicksort's recursion runs too deep.4
Heapsort in practice
Asymptotically, heapsort ties mergesort and beats quicksort's worst case. Measured on real machines it is slower, for several reasons.5
- Cache behavior. Quicksort's partition scans memory left to right, so nearly every access hits a cache line already loaded. Heapsort's inner loop jumps from index to , doubling its stride every level; once the heap outgrows the cache, most sift-down steps are cache misses. Same comparison count, very different memory-access cost.
- Comparisons per element. Each sift-down at height makes up to comparisons (find the larger child, then compare it to the sinking key), so heapsort's constant is roughly against quicksort's average .
- Small inputs. For below a few dozen, simple insertion sort beats every method: its constants are tiny, it is stable, and on nearly-sorted data it approaches linear time. Production sorts therefore cut over to insertion sort on small subproblems rather than recursing or heapifying to the bottom.
These forces meet in introsort, the algorithm behind most C++ standard
library sort implementations: run quicksort for its cache behavior and low
constants, switch any subproblem smaller than a fixed threshold to insertion
sort, and, if the recursion depth ever exceeds about (the sign of a
degenerate pivot sequence), abandon quicksort for heapsort on that
subproblem. Heapsort is rarely executed there, but it converts quicksort's
worst case into a hard ceiling with
no extra memory. Where heapsort wins outright is partial sorting: to get the
largest of elements, build a heap in and extract times, for
, far cheaper than sorting everything when .
Priority queues
Sorting is only the first use of a heap. The same structure implements a priority queue: a set of elements, each with a key (its priority), supporting
- : add to ;
- : return the element with the largest key;
- : remove and return that element;
- : raise 's key to .
A max-heap answers Maximum in , since it is just , and supports the mutating operations in , the height of the tree.5
- 1
- 2last leaf to the root
- 3
- 4callsift the new root down
- 5return
moves the other direction: bump a key and sift up, repeatedly swapping with the parent while the heap property is violated, again in . Insert is just an from : append the new element as the last leaf, then sift it up.
- 1if then
- 2error "new key smaller than current key"
- 3
- 4while and do
- 5exchange withfloat up one level
- 6
Sift-up is cheaper than sift-down per level, one comparison against the parent instead of two against children, and it touches only the ancestors of , a single path of length at most . The correctness argument mirrors sift-down's: the only edge that can violate the heap property is the one between the raised key and its parent, the swap moves the violation up one level, and it disappears at the root.
Viewed this way, heapsort is successive operations, writing each maximum into the vacancy the shrinking heap leaves behind. Priority queues built this way drive Dijkstra's shortest paths, Prim's minimum spanning tree, event-driven simulation, and any scheduler that must repeatedly serve the most urgent task.
Priority queue variants
The binary heap is the baseline priority queue; several variants trade its simple array layout for better bounds on particular operations, and the right choice depends on which operation dominates.
-ary heaps. Give each node children instead of and the tree gets shallower, height , so sift-up (one comparison per level) speeds up to . Sift-down gets slower per level, though — it must find the largest of children, comparisons — so the trade favors exactly when insertions and key-decreases outnumber extractions. That is precisely Dijkstra's and Prim's access pattern on dense graphs, where a -ary heap measurably beats a binary one. The array layout and index arithmetic generalize directly: child of node sits at .
Fibonacci and pairing heaps. The binary heap does every mutating operation in . For graph algorithms the bottleneck operation is , called once per edge, and the Fibonacci heap (Fredman and Tarjan, 1984) drives its amortized cost to , with staying amortized. Plugging it into Dijkstra improves the bound from to — asymptotically the best known for the comparison model. The catch is a large constant and a tangle of lazy trees and cut marks that make it slow in practice; the simpler pairing heap (Fredman et al., 1986) achieves nearly the same bounds with far less bookkeeping and usually wins on real inputs. These are the standard illustration that amortized and worst-case-per-operation are different design targets.
Cache-aware layouts. As with heapsort itself, the binary heap's index-doubling roams across memory and misses cache once it outgrows the L2. Layouts that reduce this — B-heaps (arranging the tree so each cache-line-sized block holds a subtree) and the array-backed -ary heaps above — keep more of each sift-down within one cache line. The same principle appears in external sorting and B-trees: on real memory hierarchies the branching factor is tuned to the block, not left at .5
Takeaways
- A binary heap is a complete binary tree stored as an array; index arithmetic (, children and ) replaces pointers, and the height is .
- The max-heap property orders only ancestor over descendant: weak enough to maintain cheaply, strong enough to keep the maximum at the root.
- (sift-down) repairs one violation in ; applies it bottom-up in , since work falls geometrically while node counts rise geometrically.
- repeatedly extracts the max into the array's tail, sorting in place in in all cases, though it is unstable and not cache-friendly.
- In practice it loses to quicksort on caches and constants; its niches are introsort's worst-case fallback, small- cutoffs aside, and partial sorting for the top .
- The same heap is a priority queue: maximum, insert, extract-max, and increase-key, the operations Dijkstra, Prim, and schedulers are built on.
Footnotes
- CLRS, Ch. 6 — Heapsort (§6.1). The binary max-heap as a complete tree stored in an array, with the max-heap property and index arithmetic for parent/children. ↩
- Erickson, Algorithms, Ch. — Data Structures. The sift-down primitive that restores the heap property at one node in . ↩
- CLRS, Ch. 6 — Heapsort (§6.3). runs in , summing geometrically decaying per-level work. ↩
- CLRS, Ch. 6 — Heapsort (§6.4). sorts in place in in all cases by repeatedly extracting the maximum. ↩
- Skiena, The Algorithm Design Manual, §4.3, §12.2 — Heaps and Priority Queues. The heap implements a priority queue with maximum and updates. ↩ ↩2 ↩3
╌╌ END ╌╌