Heap Sort

Turn the array into a max-heap, then keep moving the largest item to the end. O(n log n) in every case with O(1) extra space, traced in JS and Python.

15 minLesson 4 of 7
On this page
  1. A heap, in one minute
  2. The idea: two phases
  3. Phase 1: build the heap
  4. Phase 2: move the max to the end
  5. The code
  6. Cost
  7. Common mistakes
  8. Check yourself
  9. What to practise next

Remember selection sort: find the biggest item, put it at the end, repeat. The plan is fine. The problem is the "find the biggest" part, which scans every remaining item every time. That scan is what makes it O(n²).

Now imagine the remaining items kept the biggest one at the front for you. After you take it, the next biggest rises to the front in a handful of moves. Selection sort would become fast. That arrangement is called a heap, and the sort that uses it is heap sort.

A heap, in one minute

Picture the items as a family tree: one item at the top, and each item has at most two children below it. That shape is a binary tree. A max-heap is a binary tree where every parent is at least as big as its children. So the biggest item is always at the root. The full story is in Heaps and Priority Queues; heap sort needs only three facts:

  • The tree is stored in a plain array, level by level. The children of index i are at 2i + 1 and 2i + 2. No pointers, no extra memory.
  • The root, a[0], is the largest item.
  • Sift down repairs a heap whose root is out of place: swap the item with its bigger child until it is at least as big as both children. The tree is about log₂ n levels tall, so that takes at most about log n swaps.
9 8 5 1 3 2 index 0 9 8 5 1 3 2 0 1 2 3 4 5 array tree
The same max-heap drawn as a tree and stored as an array. Index i has its children at 2i + 1 and 2i + 2.

The idea: two phases

  1. Build a max-heap out of the whole array, in place.
  2. Repeat: swap the root (the largest item) with the last item of the heap. That largest item is now in its final sorted position, so shrink the heap by one to leave it alone. The new root is probably small, so sift it down. Stop when the heap has one item left.

The array ends up split in two: a heap at the front that shrinks, and a sorted part at the back that grows. It is selection sort with a much faster way to find the maximum.

Phase 1: build the heap

Items with no children (the second half of the array) are already tiny heaps of one. So start at the last item that has a child, at index n/2 − 1, and sift down each item from there back to the root. When you sift down an item, both of its subtrees are already heaps, so sifting it down makes its whole subtree a heap.

Build a max-heap from [3, 8, 5, 1, 9, 2]Use the arrows or ← → keys
for i from n/2 - 1 down to 0: siftDown(a, i, n)for end from n - 1 down to 1:  swap a[0] and a[end]  siftDown(a, 0, end) siftDown(a, i, size):  while i has a child below size:    c = the bigger child of i    if a[c] <= a[i]: stop    swap a[i] and a[c]; i = c
  1. a
    03
    18
    25i
    31
    49
    52
    385192

    1The array as a tree. Indexes 3, 4 and 5 have no children, so they are already heaps. The last parent is index 6/2 − 1 = 2.

  2. a
    03
    18
    25i
    31
    49
    52
    385192

    2Index 2 holds 5. Its only child is 2, which is smaller. Nothing to do.

  3. a
    03
    19i
    25
    31
    48
    52
    395182

    3Index 1 holds 8. Its children are 1 and 9. The bigger child, 9, beats 8, so swap them. 8 lands on a leaf, so it stops there.

  4. a
    09i
    13
    25
    31
    48
    52
    935182

    4Index 0 holds 3. Its children are 9 and 5. 9 is bigger than 3, so swap: 9 moves up to the root.

  5. a
    09
    18i
    25
    31
    43
    52
    985132

    5Keep sifting the 3. At index 1 its children are 1 and 8. 8 is bigger, so swap again. 3 is now on a leaf.

  6. a
    09
    18
    25
    31
    43
    52
    985132
    swaps
    3

    6Every parent is now at least as big as its children. The largest item, 9, is at the root. 3 swaps built the heap.

1 / 6

Phase 2: move the max to the end

Now the heap from the end of the last trace gets sorted. Faded tree nodes and green cells are the sorted part: they are no longer in the heap.

Sort the heap [9, 8, 5, 1, 3, 2]Use the arrows or ← → keys
for i from n/2 - 1 down to 0: siftDown(a, i, n)for end from n - 1 down to 1:  swap a[0] and a[end]  siftDown(a, 0, end) siftDown(a, i, size):  while i has a child below size:    c = the bigger child of i    if a[c] <= a[i]: stop    swap a[i] and a[c]; i = c
  1. a
    09
    18
    25
    31
    43
    52end
    985132
    heap size
    6

    1The heap covers all 6 items. The root, 9, is the largest, and it belongs in the last slot.

  2. a
    02
    18
    25
    31
    43
    59
    285139
    heap size
    5

    2Swap 9 with the last heap item, 2. 9 is final. The heap shrinks to 5 items, and its root, 2, is out of place.

  3. a
    08
    12
    25
    31
    43
    59
    825139
    heap size
    5

    3Sift 2 down. Its bigger child is 8, so swap.

  4. a
    08
    13
    25
    31
    42
    59
    835129
    heap size
    5

    4At index 1, 2's children are 1 and 3. Swap with 3. Now 2 is on a leaf, and the heap is repaired with 8 on top.

  5. a
    02
    13
    25
    31
    48
    59
    235189
    heap size
    4

    5Swap the root 8 into slot 4. The heap shrinks to 4 items.

  6. a
    05
    13
    22
    31
    48
    59
    532189
    heap size
    4

    6Sift 2 down: its children are 3 and 5. Swap with 5. Index 2 has no children inside the heap, so stop.

  7. a
    01
    13
    22
    35
    48
    59
    132589
    heap size
    3

    7Swap the root 5 into slot 3. The heap shrinks to 3 items.

  8. a
    03
    11
    22
    35
    48
    59
    312589
    heap size
    3

    8Sift 1 down: its children are 3 and 2. Swap with 3. Index 1's children, slots 3 and 4, are outside the heap, so stop.

  9. a
    02
    11
    23
    35
    48
    59
    213589
    heap size
    2

    9Swap the root 3 into slot 2. The heap shrinks to 2 items.

  10. a
    02
    11
    23
    35
    48
    59
    213589
    heap size
    2

    10Sift 2 down: its only child, 1, is smaller, so 2 stays. This time the swap did not break the heap.

  11. a
    01
    12
    23
    35
    48
    59
    123589
    heap size
    1

    11Swap the root 2 into slot 1. One item is left, and one item is always in place. Sorted.

  12. a
    01
    12
    23
    35
    48
    59
    root swaps
    5
    sift swaps
    4

    125 root swaps and 4 sift swaps. Each sift went down at most one path of the tree, never more than log₂ n levels.

1 / 12

The code

function heapSort(a) {
  const n = a.length;
  // phase 1: build a max-heap, from the last parent up to the root
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) siftDown(a, i, n);
  // phase 2: move the max to the end, shrink the heap, repair it
  for (let end = n - 1; end > 0; end--) {
    [a[0], a[end]] = [a[end], a[0]];
    siftDown(a, 0, end); // the heap is now a[0..end-1]
  }
  return a;
}
 
function siftDown(a, i, size) {
  while (true) {
    let big = i;
    const left = 2 * i + 1, right = 2 * i + 2;
    if (left < size && a[left] > a[big]) big = left;
    if (right < size && a[right] > a[big]) big = right;
    if (big === i) return; // bigger than both children: done
    [a[i], a[big]] = [a[big], a[i]];
    i = big;
  }
}
 
heapSort([3, 8, 5, 1, 9, 2]); // [1, 2, 3, 5, 8, 9]
def heap_sort(a):
    n = len(a)
    # phase 1: build a max-heap, from the last parent up to the root
    for i in range(n // 2 - 1, -1, -1):
        sift_down(a, i, n)
    # phase 2: move the max to the end, shrink the heap, repair it
    for end in range(n - 1, 0, -1):
        a[0], a[end] = a[end], a[0]
        sift_down(a, 0, end)  # the heap is now a[0..end-1]
    return a
 
def sift_down(a, i, size):
    while True:
        big = i
        left, right = 2 * i + 1, 2 * i + 2
        if left < size and a[left] > a[big]:
            big = left
        if right < size and a[right] > a[big]:
            big = right
        if big == i:  # bigger than both children: done
            return
        a[i], a[big] = a[big], a[i]
        i = big
 
heap_sort([3, 8, 5, 1, 9, 2])  # [1, 2, 3, 5, 8, 9]

Cost

Phase 2 does n − 1 sift-downs, each at most log n levels deep: O(n log n). That holds for every input, because the heap is always about log n levels tall. There is no unlucky case like quicksort's bad pivots.

Phase 1 looks like it should also be O(n log n), but it is only O(n). Half of the items are leaves and never move. A quarter sit one level above the leaves and move at most one step. Only the root can travel the full height. Added up, that is fewer than n swaps.

Everything happens with swaps inside the array, so the extra space is O(1). That combination, a guaranteed O(n log n) with no extra memory, is what heap sort offers that neither merge sort nor quicksort does.

TimeO(n log n)
SpaceO(1)

O(n) to build the heap, then n − 1 sift-downs of at most log n levels each. Same on every input. Not stable.

In practice, heap sort is usually slower than a good quicksort. Its sift-downs jump between distant parts of the array (index 1, then 3, then 7), which is unfriendly to the CPU cache. It is also not stable: the first swap of every round throws the root over everything in between.

Common mistakes

Check yourself

Quick check

In a heap stored as an array, where are the children of index 2?

Why does phase 1 start at index n/2 − 1 instead of at the last index?

You must sort 10 million numbers with a hard guarantee of O(n log n) and almost no extra memory. Which sort fits?

What to practise next

Merge sort, quicksort and heap sort all sort by comparing items, and no comparison sort can beat O(n log n). Counting and Radix Sort get under that limit by not comparing at all, and that lesson ends with a table of every sort in this module.