Heap Sort Explained: How It Works, Why It's Fast, and When to Use It
- Heap sort uses a max-heap to sort in-place with O(n log n) time and O(1) extra space.
- Phase 1 builds a max-heap in O(n) by heapifying from the last non-leaf up to the root.
- Phase 2 repeatedly swaps the root (max) with the last element and re-heapifies.
Imagine a school tournament bracket where the best player always bubbles up to the top. You crown the champion, remove them, and the bracket reorganises itself so the next-best player rises to the top — then you repeat. Heap sort does exactly this with numbers: it builds a special tree where the biggest value is always at the root, pulls that value off, fixes the tree, and repeats until everything is sorted. No extra paper needed — it all happens on the same sheet.
Every developer eventually hits a moment where a simple sort isn't good enough — maybe memory is tight, or a worst-case O(n²) from quicksort would be catastrophic in production. Heap sort was designed precisely for those moments. It's used inside priority queues, operating system schedulers, and graph algorithms like Dijkstra's — anywhere you need a guaranteed, predictable sort that never blows up under adversarial input.
The core problem heap sort solves is the tension between speed and memory. Merge sort is also O(n log n), but it needs O(n) extra space to do its work. Quicksort is fast on average but degrades to O(n²) on already-sorted or reverse-sorted data. Heap sort gives you O(n log n) in every case — best, average, and worst — while sorting the array in-place using only O(1) extra space. That's a rare combination.
By the end of this article you'll understand what a max-heap actually is and why it matters, how heapify is the secret engine behind the whole algorithm, how to implement heap sort from scratch in Java with a clear mental model you won't forget, and exactly when to reach for it over quicksort or merge sort in real projects.
How Heap Sort Works — Plain English and Algorithm
Heap sort has two phases: build a max-heap from the array, then repeatedly extract the maximum to produce a sorted array.
A max-heap is a complete binary tree where every parent is larger than its children. The maximum element is always at the root (index 0). We can represent the heap in the same array without extra space: for a node at index i, its left child is at 2i+1 and right child at 2i+2.
Phase 1 — Build a max-heap in O(n): 1. Start from the last non-leaf node (index n//2 - 1) and work backward to index 0. 2. For each node, run 'heapify down': if either child is larger, swap with the larger child and recurse. 3. After processing all nodes, the array satisfies the max-heap property.
Phase 2 — Sort by extracting elements in O(n log n): 4. Swap the root (maximum) with the last element. The maximum is now in its final sorted position. 5. Reduce the heap size by 1 (ignore the last element which is now sorted). 6. Run heapify down on the new root to restore the max-heap property. 7. Repeat steps 4-6 until the heap has one element.
Time complexity: O(n log n) for all cases. Space: O(1) — heap sort is in-place.
Worked Example — Tracing Heap Sort on [4, 10, 3, 5, 1]
Input: [4, 10, 3, 5, 1]
Phase 1 — Build max-heap. Last non-leaf = index 1 (node value 10). Heapify index 1 (value 10): children are index 3 (5) and 4 (1). 10 > both. No swap. Heapify index 0 (value 4): children are index 1 (10) and 2 (3). 10 > 4. Swap 4 and 10. Array: [10, 4, 3, 5, 1]. Now heapify the displaced 4 at index 1: children 5 and 1. 5>4. Swap. Array: [10, 5, 3, 4, 1]. Max-heap built.
Phase 2 — Sort: Iteration 1: Swap root 10 with last element 1. Array: [1, 5, 3, 4, |10]. Heap size=4. Heapify root 1: children 5, 3. 5>1. Swap. Array: [5, 1, 3, 4, |10]. Heapify 1 at index 1: children 4. 4>1. Swap. Array: [5, 4, 3, 1, |10]. Iteration 2: Swap root 5 with 1. Array: [1, 4, 3, |5, 10]. Heap size=3. Heapify root 1: children 4, 3. 4>1. Swap. Array: [4, 1, 3, |5, 10]. Heapify 1 at index 1: no children in heap. Done. Iteration 3: Swap root 4 with 3. Array: [3, 1, |4, 5, 10]. Heap size=2. Heapify root 3: child 1. 3>1. No swap. Iteration 4: Swap root 3 with 1. Array: [1, |3, 4, 5, 10]. Heap size=1. Done.
Final sorted array: [1, 3, 4, 5, 10]
Heap Sort Implementation
Heap sort is implemented in two phases. The build-heap phase constructs a max-heap in-place by calling sift-down (heapify) on every non-leaf node from right to left, taking O(n) total time (not O(n log n) — most nodes are near the bottom and require little sifting). The extraction phase repeatedly swaps the root (maximum element) with the last element of the current heap, reduces the heap size by 1, and sifts the new root down to restore the heap property. Each sift-down is O(log n), and we do n-1 extractions, giving O(n log n) total. Heap sort is not stable and has poor cache performance because of non-sequential memory access during sifting, making quicksort faster in practice despite the same asymptotic complexity.
def heap_sort(arr): n = len(arr) # Phase 1: Build max-heap for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i) # Phase 2: Extract elements one by one for i in range(n - 1, 0, -1): arr[0], arr[i] = arr[i], arr[0] # move current max to end heapify(arr, i, 0) # restore heap property def heapify(arr, heap_size, root): largest = root left = 2 * root + 1 right = 2 * root + 2 if left < heap_size and arr[left] > arr[largest]: largest = left if right < heap_size and arr[right] > arr[largest]: largest = right if largest != root: arr[root], arr[largest] = arr[largest], arr[root] heapify(arr, heap_size, largest) arr = [4, 10, 3, 5, 1] heap_sort(arr) print(arr)
| Feature / Aspect | Heap Sort | Quick Sort | Merge Sort |
|---|---|---|---|
| Best Case Time | O(n log n) | O(n log n) | O(n log n) |
| Average Case Time | O(n log n) | O(n log n) | O(n log n) |
| Worst Case Time | O(n log n) ✅ | O(n²) ❌ | O(n log n) ✅ |
| Space Complexity | O(1) in-place ✅ | O(log n) avg | O(n) ❌ |
| Stable Sort? | No ❌ | No ❌ | Yes ✅ |
| Cache Friendly? | No ❌ | Yes ✅ | Moderate |
| Real-World Speed | Slower in practice | Fastest in practice | Fast, predictable |
| Best Used When | Memory tight + worst-case matters | General purpose sorting | Stability required |
🎯 Key Takeaways
- Heap sort uses a max-heap to sort in-place with O(n log n) time and O(1) extra space.
- Phase 1 builds a max-heap in O(n) by heapifying from the last non-leaf up to the root.
- Phase 2 repeatedly swaps the root (max) with the last element and re-heapifies.
- Heap sort is not stable — equal elements may swap relative order.
- Cache performance is worse than merge sort or quick sort due to non-sequential memory access.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QHeap sort and merge sort are both O(n log n). Why does Java's
Arrays.sort()use quicksort for primitives instead of either of them? - QCan you walk me through heapify step by step? Why do we recurse downward after a swap instead of upward?
- QIf I asked you to find the median of a stream of integers in real time, how would you use two heaps to solve it — and why two instead of one?
Frequently Asked Questions
Is heap sort better than merge sort?
Heap sort has O(1) space vs O(n) for merge sort, which is an advantage. However, merge sort is stable and has better cache performance due to sequential memory access. Heap sort accesses memory in a less predictable pattern (jumping between parent and child indices), leading to more cache misses on large arrays. In practice, quick sort (with good pivot selection) outperforms both for average cases.
Why is building a heap O(n) and not O(n log n)?
Intuitively, most nodes are near the bottom of the heap and need very few swaps during heapify — only the root needs log n swaps. The mathematical summation of work across all levels converges to O(n). Starting heapify from n//2-1 down to 0 exploits this: leaf nodes (half the array) need zero work.
Is heap sort stable?
No. Heap sort is not a stable sort — equal elements may change their relative order during the heapify and extraction steps. Use merge sort if stability is required.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.