Heap Sort Explained: How It Works, Why It's Fast, and When to Use It
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.
What Is a Max-Heap? (And Why Heap Sort Needs One)
Before you can understand heap sort, you need to understand the data structure it's built on: the binary heap. Specifically, a max-heap.
A max-heap is a complete binary tree with one rule: every parent node must be greater than or equal to both of its children. That's it. It doesn't care about the order of siblings — only the parent-child relationship. The payoff is huge: the largest element in the entire collection is always sitting at the root.
Here's the clever trick: we don't actually build a tree object in memory. We store the heap as a plain array and use index arithmetic to navigate it. For any element at index i, its left child lives at 2i + 1, its right child at 2i + 2, and its parent at (i - 1) / 2. This means the 'tree' is just a mental model — the array IS the heap.
Heap sort exploits this in two phases. Phase one: rearrange the input array into a valid max-heap (the largest element ends up at index 0). Phase two: repeatedly swap the root (max) to the end of the unsorted region and shrink the heap by one, calling heapify to restore the max-heap property each time. After n swaps, the array is sorted ascending.
public class MaxHeapVisualizer { /** * Restores the max-heap property for a subtree rooted at 'rootIndex'. * 'heapSize' tells us how many elements are still part of the active heap * (elements beyond heapSize are already sorted and must not be touched). */ static void heapify(int[] array, int heapSize, int rootIndex) { int largest = rootIndex; // assume the root is the largest to start int leftChild = 2 * rootIndex + 1; // left child index in array int rightChild = 2 * rootIndex + 2; // right child index in array // Is the left child inside the heap AND larger than current largest? if (leftChild < heapSize && array[leftChild] > array[largest]) { largest = leftChild; } // Is the right child inside the heap AND larger than current largest? if (rightChild < heapSize && array[rightChild] > array[largest]) { largest = rightChild; } // If the largest element is NOT the root, we have a violation — fix it if (largest != rootIndex) { int temp = array[rootIndex]; array[rootIndex] = array[largest]; array[largest] = temp; // The swap may have broken the heap property further down — recurse heapify(array, heapSize, largest); } } public static void main(String[] args) { int[] scores = {3, 19, 7, 11, 5, 14}; System.out.println("Before heapify on full array:"); printArray(scores); // Build a max-heap from the bottom up. // We only need to heapify internal nodes (last internal node is at n/2 - 1). int n = scores.length; for (int i = n / 2 - 1; i >= 0; i--) { heapify(scores, n, i); } System.out.println("After building max-heap:"); printArray(scores); // index 0 will hold the largest value } static void printArray(int[] array) { System.out.print("[ "); for (int value : array) { System.out.print(value + " "); } System.out.println("]"); } }
[ 3 19 7 11 5 14 ]
After building max-heap:
[ 19 11 14 3 5 7 ]
Full Heap Sort Implementation — Step by Step
Now that heapify makes sense, the sort itself is almost anticlimactic — which is a sign of a well-designed algorithm.
Phase one builds the max-heap in O(n). At this point index 0 holds the maximum value of the entire array.
Phase two is a loop that runs n-1 times. Each iteration does two things: swap index 0 (the current maximum) with the last element in the active heap region, then shrink the heap size by one and call heapify at index 0 to find the new maximum. The element we just swapped to the end is now in its final sorted position — it's outside the heap and will never be touched again.
This is the in-place magic. The array is simultaneously the heap (its front portion) and the sorted output (its tail portion). No extra array needed.
One thing to notice: heap sort is not a stable sort. Swapping non-adjacent elements means equal values can change their relative order. In most systems-level scenarios this doesn't matter, but it's worth knowing before you use it on objects where original order has semantic meaning.
public class HeapSort { /** * Restores max-heap property for the subtree rooted at rootIndex. * Only elements with index < heapSize are considered part of the heap. */ static void heapify(int[] array, int heapSize, int rootIndex) { int largest = rootIndex; int leftChild = 2 * rootIndex + 1; int rightChild = 2 * rootIndex + 2; if (leftChild < heapSize && array[leftChild] > array[largest]) { largest = leftChild; } if (rightChild < heapSize && array[rightChild] > array[largest]) { largest = rightChild; } if (largest != rootIndex) { // Swap parent with the larger child to fix the violation int temp = array[rootIndex]; array[rootIndex] = array[largest]; array[largest] = temp; // The swapped child's subtree might now be violated — fix it too heapify(array, heapSize, largest); } } /** * Sorts the array in ascending order using heap sort. * Time: O(n log n) in all cases * Space: O(log n) due to recursion stack (O(1) if heapify is iterative) */ static void heapSort(int[] array) { int totalElements = array.length; // ── PHASE 1: Build a max-heap from the unsorted array ── // Start from the last internal node (index n/2 - 1) and work up to root. // Leaf nodes are trivially valid heaps, so we skip them. for (int i = totalElements / 2 - 1; i >= 0; i--) { heapify(array, totalElements, i); } // ── PHASE 2: Extract elements from the heap one by one ── for (int sortedBoundary = totalElements - 1; sortedBoundary > 0; sortedBoundary--) { // The root (index 0) is the current maximum — move it to its final position int temp = array[0]; array[0] = array[sortedBoundary]; // root goes to the end of active heap array[sortedBoundary] = temp; // last element takes the root spot // Heap just shrank by one. Restore max-heap property for the new root. heapify(array, sortedBoundary, 0); } } public static void main(String[] args) { // Real-world scenario: sorting student exam scores int[] examScores = {72, 45, 91, 38, 85, 60, 77, 55, 93, 42}; System.out.println("Unsorted exam scores:"); printArray(examScores); heapSort(examScores); System.out.println("Sorted exam scores (ascending):"); printArray(examScores); // Edge cases — good habit to always verify these int[] singleElement = {99}; heapSort(singleElement); System.out.println("Single element array: " + singleElement[0]); int[] alreadySorted = {1, 2, 3, 4, 5}; heapSort(alreadySorted); System.out.print("Already sorted input: "); printArray(alreadySorted); int[] reverseOrder = {5, 4, 3, 2, 1}; heapSort(reverseOrder); System.out.print("Reverse sorted input: "); printArray(reverseOrder); } static void printArray(int[] array) { System.out.print("[ "); for (int value : array) { System.out.print(value + " "); } System.out.println("]"); } }
[ 72 45 91 38 85 60 77 55 93 42 ]
Sorted exam scores (ascending):
[ 38 42 45 55 60 72 77 85 91 93 ]
Single element array: 99
Already sorted input: [ 1 2 3 4 5 ]
Reverse sorted input: [ 1 2 3 4 5 ]
When Should You Actually Use Heap Sort?
Knowing an algorithm is half the battle. Knowing WHEN to pull it out is what separates juniors from seniors.
Use heap sort when your absolute worst-case performance matters more than your average-case performance. Real-time systems, embedded firmware, and anything that handles adversarial input (think security-sensitive sorting) should avoid quicksort's O(n²) worst case. Heap sort guarantees O(n log n) every single time.
Use heap sort when memory is a hard constraint. If you're on embedded hardware or allocating inside a kernel module where heap allocations are forbidden, heap sort's O(1) auxiliary space beats merge sort's O(n) requirement hands down.
Don't use heap sort when cache performance matters most. Heap sort jumps around the array semi-randomly — especially during the heapify phase — which causes a lot of cache misses. Quicksort accesses memory in a more sequential pattern and runs 2-5x faster in practice on modern hardware despite the same Big-O. This is why Java's Arrays.sort() uses a dual-pivot quicksort for primitives rather than heap sort.
Don't use heap sort when stability is required. If you're sorting records with equal keys and need to preserve their original order, reach for merge sort or TimSort instead.
import java.util.PriorityQueue; /** * Real-world use case: finding the top-K highest exam scores * from a large dataset without sorting the entire array. * * Strategy: maintain a min-heap of size K. * If a new score beats the smallest score in the heap, it deserves a spot. * At the end, the heap contains exactly the K largest scores. * * Time: O(n log K) — far faster than O(n log n) when K is small * Space: O(K) — only the heap, not the full dataset */ public class TopKScores { static int[] findTopKScores(int[] allScores, int k) { if (k <= 0 || k > allScores.length) { throw new IllegalArgumentException( "k must be between 1 and " + allScores.length ); } // PriorityQueue in Java is a min-heap by default. // The smallest score in the heap sits at the front — easy to evict. PriorityQueue<Integer> minHeap = new PriorityQueue<>(k); for (int score : allScores) { if (minHeap.size() < k) { // Heap isn't full yet — add unconditionally minHeap.offer(score); } else if (score > minHeap.peek()) { // New score beats the weakest score currently in top-K minHeap.poll(); // evict the smallest minHeap.offer(score); // admit the new contender } // Otherwise, score doesn't make the cut — skip it } // Drain the heap into an array (not yet sorted within top-K) int[] topK = new int[k]; for (int i = 0; i < k; i++) { topK[i] = minHeap.poll(); } return topK; } public static void main(String[] args) { int[] classScores = { 67, 82, 91, 55, 78, 95, 60, 88, 73, 99, 45, 84, 70, 93, 61, 77, 89, 52, 96, 64 }; int topCount = 5; int[] topStudents = findTopKScores(classScores, topCount); System.out.println("Top " + topCount + " exam scores from a class of " + classScores.length + ":"); System.out.print("[ "); for (int score : topStudents) { System.out.print(score + " "); } System.out.println("]"); System.out.println("(Order within top-K reflects heap eviction order, not rank)"); } }
[ 93 95 96 99 91 ]
(Order within top-K reflects heap eviction order, not rank)
| 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's killer feature is guaranteed O(n log n) in the worst case with O(1) extra space — that combination is unique among comparison-based sorts and makes it the right choice for memory-constrained or adversarial-input scenarios.
- The array index arithmetic (left child = 2i+1, right child = 2i+2, parent = (i-1)/2) is what lets a binary heap live inside a plain array — no tree objects, no pointers, just math.
- Building the initial max-heap costs O(n), not O(n log n) — because heapify is cheap at the bottom of the tree where most nodes live. This counterintuitive fact appears in interviews regularly.
- Heap sort is not stable — equal elements can swap relative order during extraction. If stability matters, use merge sort or TimSort instead.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Starting heapify at the wrong index during heap construction — Many beginners loop from 0 to n-1 to build the heap, calling heapify on every element including leaves. This wastes work and can produce an incorrect heap order. Leaves are already valid single-element heaps. Fix: always start the build loop at n/2 - 1 (the last internal node) and go down to 0.
- ✕Mistake 2: Forgetting to pass the current heap size into heapify during phase two — If you always pass array.length as the heap size, heapify will re-include elements you've already placed in their sorted positions at the tail. Those elements get corrupted. Fix: track a shrinking sortedBoundary variable and pass it as heapSize so heapify only operates on the unsorted front portion.
- ✕Mistake 3: Assuming heap sort is always faster than merge sort because it uses less memory — O(1) space sounds like a free win, but heap sort's non-sequential memory access pattern thrashes the CPU cache on large arrays, making it slower in wall-clock time. Fix: benchmark both. If memory isn't your bottleneck, merge sort (or TimSort) will usually win on datasets that fit in cache.
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 quicksort?
It depends on your constraints. Heap sort guarantees O(n log n) even in the worst case, while quicksort degrades to O(n²) on sorted or adversarial input. But quicksort is faster in practice on typical data because it accesses memory more sequentially, which is cache-friendly. Use heap sort when worst-case guarantees or minimal memory usage matter; use quicksort when average-case speed is the priority.
Why is heap sort not stable?
Stability means equal elements keep their original relative order after sorting. Heap sort breaks this because extracting the root and placing it at the end of the array swaps non-adjacent elements — elements with equal keys can jump over each other during this process. If you need a stable sort, merge sort or TimSort (which Java uses for objects) are better choices.
What is the difference between a heap and a heap sort?
A heap is a data structure — a complete binary tree stored as an array where every parent is greater than its children (max-heap) or smaller (min-heap). Heap sort is an algorithm that exploits this structure to sort data: it first builds a max-heap from the input, then repeatedly extracts the maximum to produce a sorted array. The heap is the tool; heap sort is the technique that uses it.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.