Searching and sorting · 15 min

Heap sort: build a heap, then empty it from the back

Why building a max-heap in place costs only O(n), why swapping its root to the end and sifting down n − 1 times sorts any input in O(n log n) with O(1) extra memory, and why it still isn't stable and usually loses to quick sort.

01Why it matters

A guarantee with nothing to spare

Quick sort sorts a million random values in about 25 million comparisons, but the wrong order costs it 500 billion. Merge sort never needs more than about 20 million, but it borrows a second array as long as the first. The Linux kernel wants neither: its code runs on a small, fixed stack, where deep recursion and big buffers are unwelcome, and a sort that some input makes crawl is a weakness. So the kernel's array sort is heap sort: O(n log n) on every input, with a few variables as its only extra memory.

The idea comes from the heaps lesson: a max-heap keeps its largest value on top, so taking the top out again and again hands the values back largest first. The trick is where they go: each removal frees the last cell of the heap, and that cell is exactly where the value removed belongs.

02Key parts

The pieces you will see

max-heap
A complete binary tree in the array, with the children of index i at 2i + 1 and 2i + 2 and no child larger than its parent, so the largest value is at index 0.
build-heap
Sifting down every parent, from the last, at ⌊n/2⌋ − 1, back to the root, which makes the whole array a max-heap in O(n).
sift down
Swapping a value with its larger child while that child is larger, so it sinks until the heap holds again.
heap part and sorted part
Positions 0 to size − 1 form the heap; positions size to n − 1 hold the largest values in their final places.
stable
Equal values keep their starting order, which matters when you sort records by one field.

03See it work

Build a heap, then empty it from the back

Here are the quick sort lesson's 8 values. The tree is only in the index arithmetic: i marks the sinking value, c its larger child, and brackets under the bars show the heap part and the sorted part.

Building the heap sifts down 31, 77, 18 and 52, from the last parent, at position 3, back to the root: 77 already beats its children, the other three sink, and 77 rises to the root as 52 sinks past it, 5 swaps in all. Then each pass swaps the root into the last cell of the heap and sifts the new root down: 77 lands in position 7, then 64 in position 6, and the sorted part grows from the right. 27 comparisons and 21 swaps.

The first loop is the bottom-up build from the heaps lesson: most values sit near the bottom and barely move, so it costs O(n).

The second loop does the sorting. The root is the largest value in the heap, so swap it with the heap's last value: it now sits where it belongs, since nothing left in the heap is larger. Shrink the heap by one, so that cell is never touched again. The value that came up to the root was a leaf, usually small, so it sinks back down, swapping with its larger child.

A heap of n values is about log₂ n levels deep, and a sift-down makes at most two comparisons per level: one to find the larger child, one to compare it with the sinking value. So the n − 1 passes cost O(n log n), whatever the order of the input.

Sift-down is a loop, not a recursion, and every move is a swap inside the array, so there is no call stack to grow and no buffer. Quick sort can't promise the time, and even a careful one keeps O(log n) calls on its stack; merge sort needs the buffer. Here are heap sort and quick sort on sorted lists, quick sort's worst order:

  • Heap Sort
  • Quick Sort
  • n²/2 (reference)
  • n log₂ n (reference)
05001K1.5K2K2.5K816243240485664input size nn²/2Quick SortHeap Sortn log₂ n
At n = 64Heap Sort: about 590 comparisons, ×2.5 from n = 32Quick Sort: about 1,200 comparisons, ×3.4 from n = 32
Each point is a real run on a sorted list. Move along the chart, or use the arrow keys, to read the counts at any n.

At n = 64 quick sort makes about 1,200 comparisons and heap sort about 590. Doubling n from 32 multiplies quick sort's count by about 3.4 and heap sort's by about 2.5, close to the 2.4 of n log₂ n.

04Predict

Your turn: does it sink?

Before each step, predict whether the sinking value swaps with its larger child. It swaps only if that child is strictly larger: the test in the code panel is A[c] ≤ A[i]: stop. This list holds two 81s, so one prediction is a tie; watch where each 81 ends up.

Make 8 predictions: step forward and answer each question before the next step plays.

0 of 8 made

05Try it

The second loop in code, and what it costs

Here is the second loop on its own, on five values that already form a max-heap: 9 above 7 and 8, and 7 above 2 and 5. Each pass swaps the root with nums[end], sifts the new root down through the end values still in the heap, and prints the array.

def sift_down(a, i, size):
    while 2 * i + 1 < size:
        c = 2 * i + 1
        if c + 1 < size and a[c + 1] > a[c]:
            c += 1
        if a[c] <= a[i]:
            return
        a[i], a[c] = a[c], a[i]
        i = c

nums = [9, 7, 8, 2, 5]
for end in range(len(nums) - 1, 0, -1):
    nums[0], nums[end] = nums[end], nums[0]
    sift_down(nums, 0, end)
    print(*nums)

The whole sort puts the build in front: a sift-down over the whole array, size n, for each parent from ⌊n/2⌋ − 1 down to 0. Notice which heap it builds to sort smallest first.

Does a sorted input help, as it helps insertion sort? Here are the 12 sorted values that cost quick sort 66 comparisons in its lesson.

12 values in sorted order: a min-heap, every parent smaller than its children, so the build makes 9 swaps, and the sort takes 55 comparisons. Switch the input to Reversed: largest first is already a max-heap, so the build swaps nothing, yet the sort still takes 47. Random, the same values shuffled, takes 53.

47 to 55 comparisons in three orders, where quick sort's ranged from 35 to 66. The order mostly changes the cheap build: each pass of the second loop brings up a value from the bottom of the heap, usually small, which sinks most of the way back whatever the order.

The long swaps cost something else. In the Predict run, the 81 that started at position 1 was the first root swapped to the end and finished last, behind the 81 from position 3.

On the random order heap sort made 53 comparisons, and quick sort 35 on the same values. A better worst case doesn't make a faster sort.

06Check yourself

Check yourself

  1. Question 1 of 5

    Heap sort has built the max-heap [9, 6, 8, 2, 5, 7]. What does the array hold after the first pass of the second loop?

    Your answer comes back in Review in a few days, so it sticks.

  2. Question 2 of 5

    At most how many comparisons does a sift-down from the root of a full heap of 15 values make, counted as the visualiser counts them?

  3. Question 3 of 5

    You want an array sorted largest first, in place, with heap sort’s two loops. Which heap should the first loop build?

  4. Question 4 of 5

    Heap sort runs on [7, 7, 3]. Call the first 7 A and the second B. In what order does it leave them?

  5. Question 5 of 5

    For which of these jobs is heap sort a better choice than quick sort?

07Explain it back

Say it in your own words

How does heap sort sort an array in place? Why is it O(n log n) on every input, and what does it give up compared with quick sort and merge sort?

08Recap

What to remember

  • Heap sort builds a max-heap in the array bottom-up, in O(n), then n − 1 times swaps the root into the last cell of the heap, shrinks the heap by one and sifts the new root down.
  • The sorted part grows from the right; the part on the left is a heap, never a sorted list.
  • Each sift-down walks at most about log₂ n levels, so heap sort is O(n log n) on every input, with O(1) extra memory and no recursion.
  • To sort smallest first in place, build a max-heap: each pass fills the last cell of the heap, where the largest value left belongs.
  • It is not stable, and more comparisons and scattered memory reads make it slower than quick sort, so introsort keeps it as a fallback.
  • Next: sorting without comparisons, where counting sort and radix sort can beat n log n, and Comparison budget, which shows why no comparison sort can. Make each swap yourself in Be the algorithm, practise with Smash the Heaviest Stones, Kth Largest Number in a List and Closest Points to the Origin, or open the heap sort visualiser.
Back to path