3.4.13 · D2Trees

Visual walkthrough — Heap sort — in-place, O(n log n)

2,011 words9 min readBack to topic

This is the visual companion to Heap sort — in-place, O(n log n). If you have never seen a Binary Heap or a Complete Binary Tree, don't worry — we build every idea from zero right here.


Step 1 — An array is secretly a tree

WHAT. We start with six numbers in a plain list (an "array" — a row of boxes, each with a position number called its index, counted from ):

The small number under each value is its index. Nothing is sorted yet.

WHY. Before we can "heapify" anything, we must see the array as a tree — a pyramid of nodes where each node can have two nodes hanging below it. We do this without moving any data: the tree is just a way of reading the same array.

The three rules that turn an index into a tree position — for a node at index :

  • / : doubling the index jumps down one level of the pyramid; and pick the left vs right slot.
  • means "round down to a whole number" — it collapses both children back to the same parent.

PICTURE. Below, the flat array (top) is redrawn as a pyramid (bottom). Same numbers, same indices — just bent into a tree. The blue arrows show index reaching down to its two children at indices and .

Figure — Heap sort — in-place, O(n log n)

Step 2 — The rule we want: parent beats children (max-heap)

WHAT. We want the pyramid to obey one rule everywhere:

Read as "is greater than or equal to". A tree obeying this everywhere is a max-heap.

WHY. If every parent outranks both its kids, then the single biggest number of all cannot be anyone's child — so it must be at the very top, index . That is the whole trick: a max-heap always hands you its maximum for free.

PICTURE. Our starting tree violates the rule in several places (red edges = "child bigger than parent, illegal"). Green edges are already fine. We must fix every red edge.

Figure — Heap sort — in-place, O(n log n)
Recall Why does "every parent ≥ children" force the max to the top?

Any node that is a child is its parent, so it is not the maximum. The only node that is nobody's child is the root. Therefore the root holds the max. ::: The root is the only non-child, and everyone else is dominated by someone above them.


Step 3 — The one tool: sift-down (let a bad node sink)

WHAT. sift_down(i) fixes one node at index , assuming the two subtrees below it are already valid heaps. It compares with its two children, and if a child is bigger, swaps them — then repeats from the new position, following the value downward like a stone sinking in water.

WHY. We need a repeatable repair operation. Sifting down (not up) is right here because the node that is out of place is above correctly-ordered subtrees — the too-small value must travel down to its correct depth.

The comparison, term by term:

Here is the current heap size; any index is outside the heap and ignored (this is how leaves with only one — or zero — children are handled).

PICTURE. A value sitting on top of children and sinks: it swaps with the larger child , moves down, checks again, and stops when both its new children are smaller. Yellow = the sinking node, red arrow = the swap taken.

Figure — Heap sort — in-place, O(n log n)

Step 4 — Phase 1: build the heap from the bottom up

WHAT. We call sift_down on every internal (non-leaf) node, starting from the last internal node and walking backward to the root. The last internal node is at index

So we sift index , then , then .

WHY bottom-up? sift_down(i) demands that 's subtrees already be valid heaps. By starting at the bottom and moving up, that precondition is always met: by the time we reach a node, everything beneath it was already fixed. Leaves (indices ) are single nodes — already valid heaps — so we skip them entirely.

PICTURE. The three build steps on :

  • i = 2 (value ): children are and , both smaller → no change.
  • i = 1 (value ): children → swap with → array becomes .
  • i = 0 (value ): children → swap with ; then keeps sinking, swaps with child → array becomes .
Figure — Heap sort — in-place, O(n log n)

Final built max-heap: — the root is , the true maximum. ✔


Step 5 — Phase 2: park the max at the back, shrink, repeat

WHAT. The root is the biggest number. Swap it with the last slot still in the heap, then pretend that last slot no longer belongs to the heap (shrink the heap size by one), then sift_down(0) to repair the new root.

WHY. Each swap drops the current maximum into its final, correct, sorted position at the right end — and it never moves again. The heap shrinks, the sorted tail grows. Because we always park the largest remaining value just left of the already-sorted part, the tail comes out in ascending order.

PICTURE. One extraction on : swap ; the (green) is now frozen. Heap size drops to ; sift the new root down (swaps with ) → , a valid max-heap of size with on top.

Figure — Heap sort — in-place, O(n log n)

Step 6 — Repeat until empty: the sorted region eats the heap

WHAT. Keep doing Step 5. Each round freezes one more value on the right. After rounds the heap has one element and the whole array is sorted.

WHY. After rounds, the largest values are locked in the rightmost slots, each in its final place. The leftover heap always keeps its "max on top" property, so the next round parks the next-largest correctly. Induction finishes the sort.

PICTURE. The full film strip for → sorted. The dividing line (yellow) between the shrinking heap (left) and the growing sorted tail (green) marches leftward, one slot per round.

Figure — Heap sort — in-place, O(n log n)

Final array: . ✔


Step 7 — Edge & degenerate cases (nothing left to surprise you)

WHAT. The cases the walkthrough must not skip:

  • Empty array () or one element (): the build loop range(n//2 - 1, -1, -1) runs zero times, and Phase 2's range(n-1, 0, -1) runs zero times. Nothing happens — a - or -element list is already sorted. ✔
  • A node with only a left child. In the sift comparison, the right index can be ; we skip it. So a lone-child node compares against just its left child. This happens whenever is even (the last parent has one child).
  • All equal keys, e.g. : every comparison is a tie, no swap improves anything, so largest == i immediately and each sift stops instantly. Correct — but note the ties can still get swapped in Phase 2, which is exactly why heap sort is not stable.
  • Already-sorted input : still costs . Heap sort has no "lucky" input — a virtue over Quicksort, whose worst case is .

WHY. These are precisely where naive code breaks: an out-of-bounds child read, or an infinite loop on ties. Our index guard l < n / r < n and our largest == i stop-condition cover every one.

PICTURE. Left: even- last parent with a single (left-only) child, right slot greyed out as "outside the heap". Right: the all-equal tree where no red edges ever appear.

Figure — Heap sort — in-place, O(n log n)

The one-picture summary

Everything compressed: array→tree, build (bottom-up sifts), then rounds of swap-max-to-back → shrink → sift, with the sorted region (green) growing right-to-left.

Figure — Heap sort — in-place, O(n log n)
Recall Feynman: tell the whole walkthrough to a friend

Line up your numbers in boxes. Pretend the row is a pyramid — box on top, its two kids just below, and so on (Step 1). We want a pyramid where every card beats the two cards under it, so the biggest card floats to the very top (Step 2). To make a bad card obey, we let it sink: swap it with its bigger child over and over until both its new kids are smaller (Step 3). We fix the whole pyramid by sinking every card, starting from the lowest parent and moving up, so the parts below are always already tidy (Step 4). Now the biggest card is on top — grab it and park it at the far right end, where it's done forever; shrink the pyramid to ignore that slot and sink the new top card to fix the rule (Step 5). Do that again and again: each round parks the next-biggest just left of the last, and the sorted stretch on the right grows until the pyramid is empty and the whole row is sorted (Step 6). And it never trips: empty rows, lone children, and all-equal cards are all handled by "ignore slots past the end" and "stop when nothing's bigger" (Step 7). No second table needed — that's the in-place magic.

Connections

  • Binary Heap — the structure every picture here draws.
  • Complete Binary Tree — why the array↔tree index arithmetic never has gaps.
  • Priority Queue — same sift-down, used for live max/min access.
  • Quicksort · Merge Sort · Introsort — the sorting cousins compared in the parent note.
  • Big-O Notation — the language for the result derived here.