Heapsort
Heapsort is selection sort with a better way of selecting. Selection sort scans the unsorted region to find its maximum in ; heapsort keeps that region as a heap so the maximum is at the root and extraction costs . That single substitution converts into .
Its distinguishing property is the combination no other common sort offers: worst case in space. Mergesort needs a buffer; quicksort has a quadratic worst case; heapsort has neither problem.

Core Concepts
| Property | Value |
|---|---|
| Best case | |
| Average | |
| Worst case | — guaranteed |
| Space | — genuinely in place, no recursion |
| Stable | No |
| Adaptive | No — sorted input costs the same as random |
Mechanism
The array is used as both the heap and the output. The heap occupies a shrinking prefix; the sorted result grows as a suffix behind it.
- Python
- C++
def heapsort(a):
n = len(a)
# Phase 1: build a max-heap in place — O(n), not O(n log n)
for i in range(n // 2 - 1, -1, -1):
sift_down(a, i, n)
# Phase 2: repeatedly move the max to the end and shrink the heap
for end in range(n - 1, 0, -1):
a[0], a[end] = a[end], a[0] # largest element to its final position
sift_down(a, 0, end) # restore the heap over the remaining prefix
return a
def sift_down(a, i, size):
while True:
largest = i
for child in (2 * i + 1, 2 * i + 2):
if child < size and a[child] > a[largest]:
largest = child
if largest == i:
return
a[i], a[largest] = a[largest], a[i]
i = largest
#include <cassert>
#include <functional>
#include <queue>
#include <utility>
#include <vector>
void sift_down(std::vector<int>& a, int i, int size) {
for (;;) {
int largest = i;
for (int child : {2 * i + 1, 2 * i + 2})
if (child < size && a[child] > a[largest]) largest = child;
if (largest == i) return;
std::swap(a[i], a[largest]);
i = largest;
}
}
void heapsort(std::vector<int>& a) {
int n = static_cast<int>(a.size());
// Phase 1: build a max-heap in place — O(n), not O(n log n)
for (int i = n / 2 - 1; i >= 0; --i)
sift_down(a, i, n);
// Phase 2: repeatedly move the max to the end and shrink the heap
for (int end = n - 1; end > 0; --end) {
std::swap(a[0], a[end]); // largest element to its final position
sift_down(a, 0, end); // restore the heap over the remaining prefix
}
}
Phase 1 is — see the heaps page for why building a heap is linear rather than n log n. Phase 2 does n extractions at each, so it dominates: overall.
Using a max-heap rather than a min-heap is what makes the sort ascending: the largest element is swapped to the end, and each subsequent one lands just before it.
Tracing the first extraction on [5, 1, 8, 3]. Phase 1 builds the max-heap first (sift_down runs
on index 1, then index 0):
| Step | Action | Array |
|---|---|---|
| start | — | [5, 1, 8, 3] |
| build, i=1 | children of 1 are just index 3 (3 > 1); swap a[1] ↔ a[3] | [5, 3, 8, 1] |
| build, i=0 | children of 0 are 1, 2 (3, 8); 8 > 5, swap a[0] ↔ a[2] | [8, 3, 5, 1] |
The array is now a valid max-heap: [8, 3, 5, 1]. Phase 2's first extraction swaps the root with
the last live element, then sifts down over the heap shrunk by one:
| Step | Action | Array | Heap size |
|---|---|---|---|
| extract 1 | swap a[0] ↔ a[3] (8 ↔ 1) | [1, 3, 5, 8] | 3 |
| sift_down(0, 3) | children of 0 are 1, 2 (3, 5); 5 > 1, swap a[0] ↔ a[2] | [5, 3, 1, 8] | 3 |
| sift_down(2, 3) | index 2 has no children within size 3 | [5, 3, 1, 8] | done |
One extraction, one sift-down, and the maximum (8) is now fixed at the end in its final
sorted position — exactly like selection sort's swap-to-boundary step, but
found in instead of a full scan.
Practical Usage
Heapsort is rarely the top-level choice, but it occupies two important roles:
- The safety net in introsort. C++'s
std::sortruns quicksort, and switches to heapsort when recursion exceeds levels. This makes the worst case without giving up quicksort's speed on typical input. Heapsort is chosen for the fallback precisely because it needs no extra memory and has no bad case of its own. - Memory-constrained and real-time systems. Embedded and kernel contexts where an allocation
is unacceptable and an tail is unacceptable. The Linux kernel's
sort()is a heapsort.
- Python
- C++
# Partial sorting: the top k without sorting everything — O(n + k log n)
import heapq
def top_k(items, k):
heap = list(items)
heapq.heapify(heap) # O(n)
return [heapq.heappop(heap) for _ in range(k)] # k × O(log n)
// Partial sorting: the top k without sorting everything — O(n + k log n)
std::vector<int> top_k(std::vector<int> items, int k) {
std::priority_queue<int, std::vector<int>, std::greater<>>
heap(std::greater<>{}, std::move(items)); // heapify: O(n)
std::vector<int> out;
for (int i = 0; i < k && !heap.empty(); ++i) { // k x O(log n)
out.push_back(heap.top());
heap.pop();
}
return out;
}
Stopping phase 2 after k extractions gives the k largest elements in — better than a
full sort when k is small, which is the same argument behind heapq.nlargest.
- Python
- C++
# checked on the traced input
assert heapsort([5, 1, 8, 3]) == [1, 3, 5, 8]
assert heapsort([]) == []
int main() {
std::vector<int> a{5, 1, 8, 3};
heapsort(a);
assert((a == std::vector<int>{1, 3, 5, 8}));
}
Edge Cases & Pitfalls
Its complexity is excellent and its constant factor is not. sift_down jumps between indices i,
2i+1 and 2i+2 — locations that grow exponentially far apart, so each level of a sift is a fresh
cache miss. Quicksort's partition scans memory sequentially
and mergesort's merges are also sequential; heapsort's access pattern is nearly the worst possible.
On typical in-memory arrays it commonly runs 2–3× slower than quicksort despite identical asymptotic complexity. Choose it for its guarantees, not for its speed.
n // 2 - 1is the last internal node. Starting the build loop anywhere else either wastes work on leaves or leaves part of the heap unbuilt.sift_downmust be bounded bysize, notlen(a)— otherwise phase 2 sifts back into the already-sorted suffix and corrupts it.- It is not stable, and equal elements are reordered by the long-distance swaps.
- A min-heap sorts descending. If you want ascending output, build a max-heap.
Comparisons
| Heapsort | Quicksort | Mergesort | Selection | |
|---|---|---|---|---|
| Worst case | ||||
| Space | ||||
| Stable | No | No | Yes | No |
| Locality | Poor | Excellent | Good | Good |
| Typical speed | Slowest of the three | Fastest | Good | Very slow |
| Choose when | Guarantees + no memory | Default for arrays | Stability or external sort | Writes are costly |
Recall
References
- Williams, J.W.J. (1964), "Algorithm 232: Heapsort", Communications of the ACM — the original, which introduced the heap along with it.
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, 4th ed., Ch. 6 — heapsort with the build-heap proof.
- Linux kernel
lib/sort.c— a production heapsort, with comments on why it was chosen.
Books & Videos
- VisuAlgo — Sorting — the two phases are much clearer watched than read.
Related Pages
- Heaps & Priority Queues — the structure this is built on.
- Selection Sort — the same algorithm with a linear scan instead of a heap.
- Choosing a Sort — introsort, where heapsort is the fallback.