Home / Algorithms

Heap Sort

Build a max-heap, then repeatedly swap the root to the end and re-sink. Guaranteed O(n log n) with O(1) extra space — the only common sort that gets both at once.

Controls

array size10
# phase 1: heapify for i in range(n//2-1, -1, -1): sift_down(a, i, n) # phase 2: extract for i in range(n-1, 0, -1): a[0], a[i] = a[i], a[0] sift_down(a, 0, i)

Heap and Sorted Region

step 0

Insight

Two phases: heapify the array into a max-heap in O(n), then repeatedly swap the root to the sorted tail and sift down, n times at O(log n) each.

phase
comparisons0
swaps0
extra memoryO(1)

Complexity

Time (all cases) O(n log n)
Space O(1)
Stable No

Heap Sort

The sort that never has a bad day — and never wins a benchmark either.

Before the details

Heap sort turns the array into a max-heap, then repeatedly moves the largest element to the end. It sorts in place with a guaranteed O(n log n) in every case — best, average and worst.

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).

Phase 2: Extract n Times

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.

Selection sort with a better data structure

Heap sort has a clean derivation: selection sort repeatedly finds the largest remaining element, and finding it by scanning is O(n). A heap finds it in O(log n).

Same algorithm, better structure, and the complexity drops from O(n²) to O(n log n).

Two phases:

Build a max-heap from the array, in place, in O(n). Repeatedly swap the root (the maximum) with the last unsorted element, shrink the heap by one, and sift the new root down.

def heap_sort(arr):
    n = len(arr)

    for i in range(n // 2 - 1, -1, -1):        # build the heap: O(n)
        sift_down(arr, i, n)

    for end in range(n - 1, 0, -1):            # extract: n times O(log n)
        arr[0], arr[end] = arr[end], arr[0]    # max to its final position
        sift_down(arr, 0, end)                 # restore the heap, excluding the tail
    return arr

def sift_down(arr, i, size):
    while True:
        largest, left, right = i, 2*i + 1, 2*i + 2
        if left < size and arr[left] > arr[largest]:
            largest = left
        if right < size and arr[right] > arr[largest]:
            largest = right
        if largest == i:
            return
        arr[i], arr[largest] = arr[largest], arr[i]
        i = largest

Note that everything happens in the original array: the heap occupies the front, the sorted region grows from the back, and no auxiliary storage is needed.

Building the heap is O(n), not O(n log n)

A detail that surprises people and is worth understanding.

Sifting down from the middle backwards looks like n/2 operations each costing O(log n), suggesting O(n log n). The actual bound is O(n), because most nodes are near the bottom and have almost no distance to sift.

Half the nodes are leaves and need no work. A quarter are one level up and sift at most once. An eighth sift at most twice. Summing that series gives O(n).

Building by repeated insertion instead is O(n log n) — each insertion sifts up from a leaf, and the last insertions travel the full height. So the direction matters: sift down from the middle, not up from insertions.

Its position among the O(n log n) sorts

 Heap sortQuick sortMerge sort
Worst caseO(n log n)O(n²)O(n log n)
Extra spaceO(1)O(log n) stackO(n)
StableNoNoYes
Cache behaviourPoorExcellentGood
Typical speedSlowest of the threeFastestMiddle

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

  1. Watch phase 1. Sifting starts from the middle of the array, never the leaves — that is why heapify is O(n).
  2. 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.
  3. Watch phase 2. Each swap sends the current maximum to its final position and the green sorted region grows leftward.
  4. Compare the tree and the array. They are the same data — the tree is drawn from array indices, with no pointers anywhere.
  5. Shuffle and re-run. The operation counts barely move; heap sort has no bad inputs, unlike quick sort.

Where that leaves you

Heap sort is O(n log n) in every case with O(1) extra memory — the only common sort offering both. It loses on speed to quick sort because scattered sift-down accesses wreck cache locality, and it is not stable. Use it when worst-case guarantees matter more than raw throughput.

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:

ComponentProvides
Quick sortSpeed in the common case
Heap sortThe O(n log n) worst-case guarantee
Insertion sortLow 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.

Why it is not stable

Sifting swaps elements that are far apart in the array, so equal elements can be reordered relative to their original positions.

Sorting [3a, 3b, 1] as a max-heap: 3a becomes the root, is swapped with the last element, and 3b ends up before it. The two equal values have exchanged order.

Making it stable requires attaching original indices as a tiebreaker, which needs O(n) extra space — and that removes the in-place advantage that was the reason to choose heap sort.

So heap sort's guarantee is worst-case time and constant space, never stability. When stability matters, merge sort or Timsort is the answer.

The heap itself is far more widely used than heap sort:

Priority queues in Dijkstra's algorithm, A*, and any scheduler.

Top-k selection. A min-heap of size k gives the k largest of n items in O(n log k) time and O(k) space — better than sorting everything when k is small. This is what heapq.nlargest does.

Partial sorting. Stopping heap sort after k extractions gives the k largest elements in sorted order, in O(n + k log n).

k-way merging. A heap of the front elements of k sorted streams merges them efficiently, which is how external sorting combines runs.

Running medians. Two heaps — a max-heap of the lower half and a min-heap of the upper — give the median of a stream in O(log n) per element.

Why building the heap is O(n), not O(n log n)

The claim that heapify costs O(n) looks wrong -- there are n elements and each sift is O(log n) -- and it is the one part of heap sort worth checking rather than accepting. Count the sift steps and the answer falls out of the shape of the tree.

example_01.pyPython
Output

Questions people ask

Why is heap sort slower than quick sort if both are O(n log n)? Cache behaviour. Sifting jumps between distant indices; quick sort scans sequentially.

Is it stable? No, and making it stable requires extra space, which defeats its purpose.

Why is building the heap O(n)? Most nodes are near the leaves and sift a short distance; the series sums to O(n).

Where is it used in practice? As introsort's fallback, providing the worst-case guarantee behind quick sort's speed.

Max-heap or min-heap for ascending order? Max-heap — the largest goes to the end of the array first.

Can it sort in place? Yes, and that is its distinguishing feature: O(1) extra space with a guaranteed O(n log n).

Recap in one screen

  • Build a max-heap in O(n), then repeatedly swap the root to the end and sift down.
  • Building by sifting down from the middle is O(n); building by repeated insertion is O(n log n).
  • Uniquely among classical sorts: guaranteed O(n log n) and O(1) extra space.
  • Slower than quick sort in practice because sifting has poor cache locality, and not stable.
  • Its production role is introsort's fallback, which is what gives std::sort its worst-case guarantee.

Run it in Python

Two phases, printed separately: build a max-heap out of the raw list, then repeatedly move the root to the end. Nothing is allocated — the heap and the sorted output share one list.

heap_sort.pyPython 3
Output

How the code works

  1. child = 2 * root + 1The tree is only an idea — the list is the tree. Node i's children are at 2i+1 and 2i+2, so no pointers, and no node objects, are needed.
  2. if a[root] >= a[child]: returnThe early exit. A sift stops as soon as the heap property holds, which is what keeps each one at O(log n) rather than always walking to a leaf.
  3. for start in range(n // 2 - 1, -1, -1):Everything past n // 2 - 1 is a leaf, and a leaf is a valid heap on its own. Building bottom-up like this is O(n) — not O(n log n), which is the surprising part.
  4. a[0], a[end] = a[end], a[0]The root is the largest remaining value, so it belongs at the end of the unsorted region. One swap both extracts it and puts it in final position.
  5. sift_down(a, 0, end)end shrinks each round, so the same list holds a shrinking heap on the left and a growing sorted run on the right. That is why heap sort needs O(1) extra space.

Change one thing

  • Print a after phase 1 only. It is not sorted — a heap is a much weaker ordering than a sorted list, and that weakness is what makes it cheap to build.
  • Flip both comparisons in sift_down to build a min-heap. The output comes out descending.
  • Sort [3, 1, 3] and follow the two 3s. They swap order — heap sort is not stable, unlike merge sort.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. After the heapify phase, the list is:

  2. Building the heap bottom-up, starting at the last parent, costs:

  3. Why does the extraction phase swap the root with the last item?

Cheat sheet

Heap Sort

Build a max-heap, then repeatedly swap the root to the end and re-sink. Guaranteed O(n log n) with O(1) extra space — the only common sort that gets both at once.

ALGORITHMS · vizlearn.in/dsa/heap_sort.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.