Heap sort
Heap sort turns the array into a max-heap and then repeatedly moves the largest element to the end. It sorts in place with a guaranteed O(n log n) worst case, but it is not stable and it is usually slower than quick sort in practice.
Learning goals
- Describe a binary heap stored in an array and compute parent and child indices
- Implement siftDown and build a heap in O(n)
- Trace heap sort's two phases
- Explain why heap sort is in place but not stable
Prerequisites
Heapify
A max-heap is a complete binary tree where every parent is at least as large as its children. Stored in an array, the children of index i are 2i + 1 and 2i + 2, and the parent of i is (i - 1) / 2. No pointers are needed.
siftDown(i) fixes a single violation: if a child is larger than the node, swap the node with its larger child and continue from there. It assumes both subtrees are already heaps. The animation shows the array both as a tree and as boxes.
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(1)
Heap sort
Interactive · edit the inputHeap as a binary tree · 8 nodes
4, 10, 3, 5, 1, 8, 7, 2
int[] arr · length 8
Phase 1 turns the array into a max-heap, where every parent is >= its children. Phase 2 repeatedly moves the maximum to the end.
1public static void heapSort(int[] arr) {2int n = arr.length;3for (int i = n / 2 - 1; i >= 0; i--) {4siftDown(arr, n, i); // build a max-heap5}6for (int end = n - 1; end > 0; end--) {7swap(arr, 0, end); // largest goes to its final slot8siftDown(arr, end, 0); // repair the smaller heap9}10}1112static void siftDown(int[] arr, int size, int i) {13while (true) {14int largest = i, l = 2 * i + 1, r = 2 * i + 2;15if (l < size && arr[l] > arr[largest]) largest = l;16if (r < size && arr[r] > arr[largest]) largest = r;17if (largest == i) return;18swap(arr, i, largest);19i = largest;20}21}
Live state
n
8
Execution trace (dry run table)Show 102 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=8 | Phase 1 turns the array into a max-heap, where every parent is >= its children. Phase 2 repeatedly moves the maximum to the end. |
| 2 | L3 | i=3 n=8 | Heapify the subtree rooted at 3. Indices 4..7 are leaves and are already heaps. |
| 3 | L14 | i=3 size=8 l=7 r=8 | Sift down from index 3 (5). Its children are at 7 and 8; 8 is outside the heap. |
| 4 | L15 | i=3 l=7 largest=3 | The left child is not larger. |
| 5 | L17 | i=3 largest=3 | 5 is at least as large as both children, so the heap property holds here. |
| 6 | L3 | i=2 n=8 | Heapify the subtree rooted at 2. Indices 4..7 are leaves and are already heaps. |
| 7 | L14 | i=2 size=8 l=5 r=6 | Sift down from index 2 (3). Its children are at 5 and 6. |
| 8 | L15 | i=2 l=5 largest=2 | The left child is larger. |
| 9 | L16 | i=2 r=6 largest=5 | The right child is not larger. |
| 10 | L17 | i=2 largest=5 | A child is larger than 3, so swap with the larger child. |
| 11 | L18 | i=2 largest=5 | Swapped arr[2] and arr[5]. Continue sifting from 5. |
| 12 | L14 | i=5 size=8 l=11 r=12 | Sift down from index 5 (3). Its children are at 11 and 12, both outside the heap. |
| 13 | L17 | i=5 largest=5 | 3 is at least as large as both children, so the heap property holds here. |
| 14 | L3 | i=1 n=8 | Heapify the subtree rooted at 1. Indices 4..7 are leaves and are already heaps. |
| 15 | L14 | i=1 size=8 l=3 r=4 | Sift down from index 1 (10). Its children are at 3 and 4. |
| 16 | L15 | i=1 l=3 largest=1 | The left child is not larger. |
| 17 | L16 | i=1 r=4 largest=1 | The right child is not larger. |
| 18 | L17 | i=1 largest=1 | 10 is at least as large as both children, so the heap property holds here. |
| 19 | L3 | i=0 n=8 | Heapify the subtree rooted at 0. Indices 4..7 are leaves and are already heaps. |
| 20 | L14 | i=0 size=8 l=1 r=2 | Sift down from index 0 (4). Its children are at 1 and 2. |
| 21 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 22 | L16 | i=0 r=2 largest=1 | The right child is not larger. |
| 23 | L17 | i=0 largest=1 | A child is larger than 4, so swap with the larger child. |
| 24 | L18 | i=0 largest=1 | Swapped arr[0] and arr[1]. Continue sifting from 1. |
| 25 | L14 | i=1 size=8 l=3 r=4 | Sift down from index 1 (4). Its children are at 3 and 4. |
| 26 | L15 | i=1 l=3 largest=1 | The left child is larger. |
| 27 | L16 | i=1 r=4 largest=3 | The right child is not larger. |
| 28 | L17 | i=1 largest=3 | A child is larger than 4, so swap with the larger child. |
| 29 | L18 | i=1 largest=3 | Swapped arr[1] and arr[3]. Continue sifting from 3. |
| 30 | L14 | i=3 size=8 l=7 r=8 | Sift down from index 3 (4). Its children are at 7 and 8; 8 is outside the heap. |
| 31 | L15 | i=3 l=7 largest=3 | The left child is not larger. |
| 32 | L17 | i=3 largest=3 | 4 is at least as large as both children, so the heap property holds here. |
| 33 | L5 | n=8 | Max-heap built: the largest value 10 is at the root. |
| 34 | L7 | end=7 | Swap the root with arr[7]. 10 is now in its final position and the heap shrinks to 7 elements. |
| 35 | L14 | i=0 size=7 l=1 r=2 | Sift down from index 0 (2). Its children are at 1 and 2. |
| 36 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 37 | L16 | i=0 r=2 largest=1 | The right child is larger still. |
| 38 | L17 | i=0 largest=2 | A child is larger than 2, so swap with the larger child. |
| 39 | L18 | i=0 largest=2 | Swapped arr[0] and arr[2]. Continue sifting from 2. |
| 40 | L14 | i=2 size=7 l=5 r=6 | Sift down from index 2 (2). Its children are at 5 and 6. |
| 41 | L15 | i=2 l=5 largest=2 | The left child is larger. |
| 42 | L16 | i=2 r=6 largest=5 | The right child is larger still. |
| 43 | L17 | i=2 largest=6 | A child is larger than 2, so swap with the larger child. |
| 44 | L18 | i=2 largest=6 | Swapped arr[2] and arr[6]. Continue sifting from 6. |
| 45 | L14 | i=6 size=7 l=13 r=14 | Sift down from index 6 (2). Its children are at 13 and 14, both outside the heap. |
| 46 | L17 | i=6 largest=6 | 2 is at least as large as both children, so the heap property holds here. |
| 47 | L7 | end=6 | Swap the root with arr[6]. 8 is now in its final position and the heap shrinks to 6 elements. |
| 48 | L14 | i=0 size=6 l=1 r=2 | Sift down from index 0 (2). Its children are at 1 and 2. |
| 49 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 50 | L16 | i=0 r=2 largest=1 | The right child is larger still. |
| 51 | L17 | i=0 largest=2 | A child is larger than 2, so swap with the larger child. |
| 52 | L18 | i=0 largest=2 | Swapped arr[0] and arr[2]. Continue sifting from 2. |
| 53 | L14 | i=2 size=6 l=5 r=6 | Sift down from index 2 (2). Its children are at 5 and 6; 6 is outside the heap. |
| 54 | L15 | i=2 l=5 largest=2 | The left child is larger. |
| 55 | L17 | i=2 largest=5 | A child is larger than 2, so swap with the larger child. |
| 56 | L18 | i=2 largest=5 | Swapped arr[2] and arr[5]. Continue sifting from 5. |
| 57 | L14 | i=5 size=6 l=11 r=12 | Sift down from index 5 (2). Its children are at 11 and 12, both outside the heap. |
| 58 | L17 | i=5 largest=5 | 2 is at least as large as both children, so the heap property holds here. |
| 59 | L7 | end=5 | Swap the root with arr[5]. 7 is now in its final position and the heap shrinks to 5 elements. |
| 60 | L14 | i=0 size=5 l=1 r=2 | Sift down from index 0 (2). Its children are at 1 and 2. |
| 61 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 62 | L16 | i=0 r=2 largest=1 | The right child is not larger. |
| 63 | L17 | i=0 largest=1 | A child is larger than 2, so swap with the larger child. |
| 64 | L18 | i=0 largest=1 | Swapped arr[0] and arr[1]. Continue sifting from 1. |
| 65 | L14 | i=1 size=5 l=3 r=4 | Sift down from index 1 (2). Its children are at 3 and 4. |
| 66 | L15 | i=1 l=3 largest=1 | The left child is larger. |
| 67 | L16 | i=1 r=4 largest=3 | The right child is not larger. |
| 68 | L17 | i=1 largest=3 | A child is larger than 2, so swap with the larger child. |
| 69 | L18 | i=1 largest=3 | Swapped arr[1] and arr[3]. Continue sifting from 3. |
| 70 | L14 | i=3 size=5 l=7 r=8 | Sift down from index 3 (2). Its children are at 7 and 8, both outside the heap. |
| 71 | L17 | i=3 largest=3 | 2 is at least as large as both children, so the heap property holds here. |
| 72 | L7 | end=4 | Swap the root with arr[4]. 5 is now in its final position and the heap shrinks to 4 elements. |
| 73 | L14 | i=0 size=4 l=1 r=2 | Sift down from index 0 (1). Its children are at 1 and 2. |
| 74 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 75 | L16 | i=0 r=2 largest=1 | The right child is not larger. |
| 76 | L17 | i=0 largest=1 | A child is larger than 1, so swap with the larger child. |
| 77 | L18 | i=0 largest=1 | Swapped arr[0] and arr[1]. Continue sifting from 1. |
| 78 | L14 | i=1 size=4 l=3 r=4 | Sift down from index 1 (1). Its children are at 3 and 4; 4 is outside the heap. |
| 79 | L15 | i=1 l=3 largest=1 | The left child is larger. |
| 80 | L17 | i=1 largest=3 | A child is larger than 1, so swap with the larger child. |
| 81 | L18 | i=1 largest=3 | Swapped arr[1] and arr[3]. Continue sifting from 3. |
| 82 | L14 | i=3 size=4 l=7 r=8 | Sift down from index 3 (1). Its children are at 7 and 8, both outside the heap. |
| 83 | L17 | i=3 largest=3 | 1 is at least as large as both children, so the heap property holds here. |
| 84 | L7 | end=3 | Swap the root with arr[3]. 4 is now in its final position and the heap shrinks to 3 elements. |
| 85 | L14 | i=0 size=3 l=1 r=2 | Sift down from index 0 (1). Its children are at 1 and 2. |
| 86 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 87 | L16 | i=0 r=2 largest=1 | The right child is larger still. |
| 88 | L17 | i=0 largest=2 | A child is larger than 1, so swap with the larger child. |
| 89 | L18 | i=0 largest=2 | Swapped arr[0] and arr[2]. Continue sifting from 2. |
| 90 | L14 | i=2 size=3 l=5 r=6 | Sift down from index 2 (1). Its children are at 5 and 6, both outside the heap. |
| 91 | L17 | i=2 largest=2 | 1 is at least as large as both children, so the heap property holds here. |
| 92 | L7 | end=2 | Swap the root with arr[2]. 3 is now in its final position and the heap shrinks to 2 elements. |
| 93 | L14 | i=0 size=2 l=1 r=2 | Sift down from index 0 (1). Its children are at 1 and 2; 2 is outside the heap. |
| 94 | L15 | i=0 l=1 largest=0 | The left child is larger. |
| 95 | L17 | i=0 largest=1 | A child is larger than 1, so swap with the larger child. |
| 96 | L18 | i=0 largest=1 | Swapped arr[0] and arr[1]. Continue sifting from 1. |
| 97 | L14 | i=1 size=2 l=3 r=4 | Sift down from index 1 (1). Its children are at 3 and 4, both outside the heap. |
| 98 | L17 | i=1 largest=1 | 1 is at least as large as both children, so the heap property holds here. |
| 99 | L7 | end=1 | Swap the root with arr[1]. 2 is now in its final position and the heap shrinks to 1 element. |
| 100 | L14 | i=0 size=1 l=1 r=2 | Sift down from index 0 (1). Its children are at 1 and 2, both outside the heap. |
| 101 | L17 | i=0 largest=0 | 1 is at least as large as both children, so the heap property holds here. |
| 102 | L10 | Sorted in place with 27 comparisons and 20 swaps. Equal keys can swap past each other, so heap sort is not stable. |
Build heap
To build a heap from an arbitrary array, call siftDown on every non-leaf node from the last parent n/2 - 1 down to 0. The leaves are already one-element heaps.
This costs O(n), not O(n log n). A node at height h sifts down at most h levels, and there are about n / 2^(h+1) nodes at height h. The sum of h · n / 2^(h+1) over all h is O(n), because the series Σ h / 2^h converges to 2.
Calling add n times (sift up) also builds a heap, but costs O(n log n).
Repeated extract maximum
Phase 2: the maximum is at index 0. Swap it with the last element of the heap, shrink the heap size by one so that element is now outside it and finished, then siftDown(0) to restore the heap property in the smaller heap.
Repeat until the heap has one element. Each extraction costs O(log n), so phase 2 costs O(n log n). The tail of the array fills with the largest values in increasing order, which is why the final array is ascending.
| Phase | Work | Cost |
|---|---|---|
| Build max-heap | siftDown for n/2 nodes | O(n) |
| Extract n - 1 times | swap + siftDown(0) | O(n log n) |
| Total | O(n log n) |
In-place property
Heap sort needs only a few variables, so it uses O(1) extra memory. Merge sort needs O(n) and quick sort needs O(log n) stack. Together with the worst-case guarantee that is what makes heap sort attractive for memory-constrained systems and for safety-critical code that cannot tolerate quick sort's quadratic worst case.
Quick sort usually beats it in practice. Heap sort jumps between index i and 2i + 1, which is poor for CPU caches, and it does more comparisons: about 2n log₂ n against 1.39 n log₂ n.
Instability
Heap sort is not stable. Swapping the root with the last element moves an element across the entire array, and sifting moves it between levels, so equal keys can pass each other.
Example: for [2a, 2b] the build phase does nothing. Extraction swaps the root 2a with 2b, giving [2b, 2a]: order reversed. Run the “Duplicates” preset and compare the a/b tags.
Practice ladder
Now try it yourself
Easy
- Given an index, compute its parent and children
- Is this array a valid max-heap?
Medium
- Implement siftDown and heapify
- Sort an array in descending order with a min-heap variant
- Kth largest element with a size-k min-heap
Advanced
- Prove that building a heap is O(n)
- Bottom-up heap sort that sifts the hole to a leaf first and saves comparisons
- Smoothsort (adaptive heap sort) overview
Key takeaways
- Heap sort is O(n log n) in every case, in place, and not stable.
- Build the heap in O(n) with siftDown from the last parent; then extract the maximum n - 1 times.
- In practice it loses to quick sort on cache behaviour, but it has no bad inputs.