Before the details
That worst-case guarantee is its strongest property. Quicksort averages O(n log n) but degrades to O(n²) on adversarial input.
Phase 1: Heapify in O(n)
Starting from the last non-leaf node and working backwards, sift each element down. Leaves are already valid heaps of size one, so half the array needs no work at all.
It looks like it should be O(n log n), but it is O(n). Most nodes are near the bottom and can only sink a short distance; only the root can travel the full height. Summing the actual work gives a series that converges to O(n).
The maximum is always at index 0. Swap it with the last element of the heap, shrink the heap by one, and sift the new root down to restore the heap property.
The array splits into two regions: an unsorted heap at the front and a growing sorted tail at the back. Watch the green region grow from the right as you step.
Each extraction costs O(log n) and there are n of them → O(n log n). Adding the O(n) heapify leaves O(n log n) overall.
Why It Loses to Quick Sort in Practice
Heap sort has better worst-case guarantees than quick sort (which can degrade to O(n²)) and uses less memory than merge sort (which needs O(n)). Yet quick sort is usually faster in the real world.
The reason is cache locality. Sifting jumps between indices i, 2i+1 and 2i+2 — wildly scattered addresses that defeat the CPU cache. Quick sort scans contiguously and stays cache-friendly. Heap sort also does more swaps and is not stable.
Its real niche is guaranteed worst-case behaviour with constant memory — embedded and real-time systems. It also appears inside introsort (used by C++'s std::sort), which starts with quick sort and switches to heap sort if recursion gets too deep, capping the worst case.
Its position among the O(n log n) sorts
| | Heap sort | Quick sort | Merge sort |
|---|
| Worst case | O(n log n) | O(n²) | O(n log n) |
| Extra space | O(1) | O(log n) stack | O(n) |
| Stable | No | No | Yes |
| Cache behaviour | Poor | Excellent | Good |
| Typical speed | Slowest of the three | Fastest | Middle |
Heap sort is the only one with both a guaranteed O(n log n) and O(1) extra space. That combination is unique among the classical sorts, and it is why the algorithm survives.
It is also the slowest of the three in practice, and the reason is cache behaviour: sifting jumps between indices i, 2i+1 and 2i+2, which on a large array means touching memory pages far apart. Quick sort's partitioning scans sequentially, which modern CPUs handle far better.
So heap sort is rarely chosen on its own — and it is chosen as a fallback, which is a genuinely important role.
Exploration guide
- Watch phase 1. Sifting starts from the middle of the array, never the leaves — that is why heapify is O(n).
- Note when the heap is valid. Every parent now beats its children, but the array is far from sorted — a heap is only partially ordered.
Introsort: where heap sort actually runs
C++'s std::sort is introsort, and heap sort is its safety net.
The algorithm starts with quick sort. It tracks recursion depth, and if the depth exceeds roughly 2 log n — evidence that pivots are being chosen badly and the O(n²) case is developing — it switches to heap sort for that subarray. Below a small threshold it finishes with insertion sort.
The result combines all three strengths:
| Component | Provides |
|---|
| Quick sort | Speed in the common case |
| Heap sort | The O(n log n) worst-case guarantee |
| Insertion sort | Low overhead on small subarrays |
That is heap sort's real production role: it is never the fastest choice, and it is the one that makes quick sort's worst case impossible. Without it, std::sort would have no guarantee.
Python takes a different route — Timsort, which is a stable merge sort — because Python guarantees stability and heap sort is not stable.