Exercises — Heap sort — O(n log n), in-place, not stable
Throughout, arrays are 0-indexed and we build a max-heap (every parent its children). Recall the three index formulas:
Level 1 — Recognition
L1.1 — Is it a heap?
Consider the array . Draw it as a tree and decide: is it a valid max-heap?
Recall Solution
Place the values level by level, left to right. Index 0 is the root, indices 1–2 are the next level, indices 3–5 the last.

Check every parent against its children:
- Node 0 (): children are index 1 () and index 2 (). and . ✅
- Node 1 (): children are index 3 () and index 4 (). and . ✅
- Node 2 (): children are index 5 () and index 6 (does not exist). . ✅
Every parent dominates its children, so yes, it is a valid max-heap.
L1.2 — Find the family
In , what is the parent, left child, and right child of the node at index (value )?
Recall Solution
Plug into the formulas.
- parent → value .
- left child → value .
- right child → value .
So 's parent is , and its children are and . (Sanity: and , so the heap property holds here.)
Level 2 — Application
L2.1 — One sift-down
Given the array (heap size ), run sift_down(A, 0, 5) on the root. Show the array after it finishes.
Recall Solution
The root () is too small; sift-down pushes it down until both its children are it.

Iteration 1: , children (), (). Largest of is index 1. Swap indices 0 and 1: Now follow the demoted : .
Iteration 2: , children (), (). Largest of is index 4. Swap indices 1 and 4: Now .
Iteration 3: . Its left child would be , so it has no children. largest == i, loop breaks.
Final array: — a valid max-heap.
L2.2 — Build-max-heap by hand
Turn () into a max-heap. The build loop starts at and counts down to . Show the array after each sift_down.
Recall Solution
We only sift the internal (non-leaf) nodes, indices . Leaves (indices 3,4,5) are already trivially heaps.
(value ): children (), (none). Largest of is index 2 itself. No swap.
(value ): children (), (). Largest of is index 3. Swap 1↔3: Follow to : child , no children. Stop.
(value ): children (), (). Largest of is index 2. Swap 0↔2: Follow to : children (), (none). Largest of is index 5. Swap 2↔5: Follow to : leaf. Stop.
Final heap: . Verify root is the maximum — good.
Level 3 — Analysis
L3.1 — Why to build, not
The naive guess: build calls sift-down on nodes, each costing , so build is . But the true bound is . Explain the gap.
Recall Solution
The naive bound is not wrong, just loose. It over-charges by assuming every node can fall the full height . But most nodes are near the bottom and can barely fall at all.
Count by height (leaves have ). A tree of nodes has at most nodes at height , and each can fall at most levels. Total work: Why is exactly ? Write it out and use a shift trick. Let Halve every term (this shifts the sequence one slot right): Subtract the second line from the first. Each coefficient drops by exactly , leaving a plain geometric series: So , giving . (The geometric series sums to because it is .) Therefore
Picture: the many cheap leaves (half the nodes, cost 0) dwarf the few expensive nodes near the root. The geometric decay of beats the linear growth of .
L3.2 — Best case is still
Merge sort and heap sort are both . But quicksort has a bad case and insertion sort has a lovely best case. What is heap sort's best case, and why can't it be faster?
Recall Solution
Even on an already-sorted or all-equal array, the sort-down phase must do its full work: times it swaps the root to the back and calls sift-down on a heap of shrinking size . Each such sift-down on the root of a size- heap costs in the worst path, and — crucially — the value swapped up from the back is a small leaf value, so it almost always sinks the full height.
Summing the sort-down cost: Why is ? Bound it from both sides.
- Upper bound: every factor in is , so , hence . That gives the side.
- Lower bound: the top half of the factors, from up to (there are of them), are each . So , hence .
Squeezed between and , we get . (This is the elementary version of Stirling's approximation .) There is no input that lets sift-down "get lucky and stop early" across all rounds. Hence heap sort is in best, average, and worst cases — remarkably consistent, unlike quicksort.
Level 4 — Synthesis
L4.1 — Full trace, first two crownings
Take the heap from L2.2, . Run the sort-down phase: swap(A[0], A[end]); sift_down(A,0,end) for end = 5 then end = 4. Show the array after each of these two crownings. Which values are now locked in the sorted tail?
Recall Solution
end = 5. Swap root and index 5: . The is now locked in place at the end. Heap shrinks to indices ; sift-down index 0 over :
- : children (), (). Largest index 1. Swap 0↔1: .
- : children (), (). Largest of is index 1 itself. Stop.
- Array: .
end = 4. Swap index 0 and index 4: . The is locked. Heap shrinks to indices ; sift-down index 0 over :
- : children (), (). Largest index 2. Swap 0↔2: .
- : child , no children. Stop.
- Array: .
After two crownings the array is and the sorted tail holds .
L4.2 — Complete the sort
Continue L4.1 from (active heap = first 4 slots) to the fully sorted array. Show every crowning.
Recall Solution
end = 3. Swap 0↔3: . Sift-down 0 over :
- : children (), (). Largest index 1. Swap: .
- : child , no children. Stop → .
end = 2. Swap 0↔2: . Sift-down 0 over :
- : child (). Largest is index 0. Stop → .
end = 1. Swap 0↔1: . Sift-down 0 over : no children. Loop end ends at 1.
Final: — fully sorted. ✅
Level 5 — Mastery
L5.1 — Stability counterexample (find the minimum size)
Heap sort is not stable. Find the smallest array of records with a key and a tag that proves it — i.e. two equal keys end up in swapped original order after sorting. Justify why your size is minimal.
Recall Solution
"Stable" means equal keys keep their original left-to-right order. We need two equal keys that heap sort reorders. Write records as , sorting ascending by key.
The minimum is . (You cannot do it with : with or record there is no pair of equal keys to reorder at all.) Take (two equal keys, before ):
- Build: the only internal node is index . sift-down(0) over : child index 1 has key , which is not strictly greater than the root's key , so no swap. Heap stays .
- Sort-down end=1: swap root and index 1: , then lock index 1. sift-down(0) over the shrunk : no children, nothing happens.
- Final: .
The single final swap flips the equal keys from to — original order broken. So the smallest witness is just two equal elements, and that lone crowning swap is the whole reason heap sort is unstable.
(Aside: a -element array like also fails, but it is not minimal — the is irrelevant to the instability, which lives entirely in the equal-key swap.)
L5.2 — Degenerate inputs
State precisely what heap sort does on: (a) an empty array , (b) a single element , (c) all-equal . No swaps happen in some — say which and why.
Recall Solution
(a) , . Build loop starts at and counts down to 0 — it never runs (start already below 0). Sort-down loop runs end from down to 1 — never runs. Zero operations, output . Correctly sorted (vacuously).
(b) , . Build starts at down to 0 → doesn't run. Sort-down runs end from down to → doesn't run. Zero swaps, output .
(c) , . Build sifts . Every comparison uses strict >; since all values are equal, no child is ever strictly greater, so largest never moves — no swaps in build. Sort-down still runs times, each swapping root with the back (swapping equal values — harmless) then sifting with no strict-greater child, so no further movement. Output , sorted. Note: the swaps here are "wasted work" but the strict > guard keeps sift-down from looping forever.
Recall Self-check (how to use these)
The lines below are active recall prompts. For each ==highlighted== term, cover the sentence and try to say the highlighted word before revealing it. For each Question ::: Answer line, read only the part before :::, answer aloud, then check yourself against the part after :::. In Obsidian these render as click-to-reveal.
A heap sort's build phase costs == while the sort-down phase costs . sift-down assumes the node's subtrees are already valid heaps. Heap sort is not stable because the build/crown swaps move equal keys across each other==. Best case for heap sort ::: — no input lets it finish faster. Why start build at index ::: it is the last internal (non-leaf) node; everything after is a leaf and already a heap.