Skip to content
Home DSA Heap Sort Explained: How It Works, Why It's Fast, and When to Use It

Heap Sort Explained: How It Works, Why It's Fast, and When to Use It

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Sorting → Topic 6 of 8
Heap sort demystified — understand the max-heap, heapify, and in-place sorting with runnable Java code, real gotchas, and interview prep tips.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
Heap sort demystified — understand the max-heap, heapify, and in-place sorting with runnable Java code, real gotchas, and interview prep tips.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

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.

heap_sort.py · PYTHON
1234567891011121314151617181920212223242526272829
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)
▶ Output
[1, 3, 4, 5, 10]
Feature / AspectHeap SortQuick SortMerge Sort
Best Case TimeO(n log n)O(n log n)O(n log n)
Average Case TimeO(n log n)O(n log n)O(n log n)
Worst Case TimeO(n log n) ✅O(n²) ❌O(n log n) ✅
Space ComplexityO(1) in-place ✅O(log n) avgO(n) ❌
Stable Sort?No ❌No ❌Yes ✅
Cache Friendly?No ❌Yes ✅Moderate
Real-World SpeedSlower in practiceFastest in practiceFast, predictable
Best Used WhenMemory tight + worst-case mattersGeneral purpose sortingStability 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

    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.
    Fix

    always start the build loop at n/2 - 1 (the last internal node) and go down to 0.

    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.
    Fix

    track a shrinking sortedBoundary variable and pass it as heapSize so heapify only operates on the unsorted front portion.

    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.
    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 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.

🔥
Naren Founder & Author

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.

← PreviousQuick SortNext →Counting Sort and Radix Sort
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged