3.4.11 · D5Trees

Question bank — Heapify — bottom-up O(n) build

1,789 words8 min readBack to topic

Before you start, re-anchor every symbol and convention this whole page turns on:

The tree below is the mental picture for the whole page — burn the index layout in before reading a single trap.

Figure — Heapify — bottom-up O(n) build

Two pictures back the cost argument, so glance at them now and refer back whenever a "why" item mentions counts or the series:

Figure — Heapify — bottom-up O(n) build
Figure — Heapify — bottom-up O(n) build

True or false — justify

Every leaf of a complete binary tree is already a valid heap, so we never call siftDown on it.
True. A leaf has no children, so it cannot violate parent≥child; calling siftDown on it is wasted work and could even index a child past the array end (out of bounds).
Bottom-up heapify is because each siftDown is .
False. A single siftDown can cost up to (the root's). The total is because tall, expensive nodes are exponentially rare — not because any one call is cheap.
is a wrong upper bound for bottom-up heapify.
False. It is a correct upper bound — just not a tight one. Every node truly costs at most , so the sum is at most ; the sharper analysis shows it's actually .
The linear cost comes from the series converging to the constant .
True. That value is derived in the Why questions section below (differentiate the geometric series, then plug ). Because the weighted sum is a finite constant independent of , the total work is ; the "" you'd expect gets killed by the exponentially shrinking node counts.
Building a heap by inserting elements one by one (siftUp each) is also .
False. Insertion build is . siftUp moves the numerous leaves toward the root — the expensive direction — whereas siftDown pushes expensive travel onto the rare tall nodes.
After bottom-up heapify, the array is fully sorted.
False. It only satisfies the heap property (parent≥children in a max-heap). The largest element sits at index 0, but the rest are only partially ordered; sorting requires Heapsort on top.
For the same input array, bottom-up siftDown and repeated siftUp always produce the identical final array.
False. Both yield a valid heap, but the arrangements can differ. Many valid heaps exist for one multiset; the two algorithms take different swap paths.
Node heights are counted from the root downward.
False. In this analysis height is counted from the bottom: leaves = height 0, root = height . Counting from the top is depth, and the cost argument breaks if you mix them up.
Doubling roughly doubles the total heapify work.
True. Since the cost is , work scales linearly — double the elements, double the work (asymptotically), unlike which grows a bit faster.

Spot the error

"Start buildHeap at index and sift each node down to the root."
Two errors: the start index should be the last internal node (leaves need no siftDown), and siftDown moves down, never toward the root.
"Process nodes top-down (index 0 upward) so we fix the root first."
Wrong order. siftDown(i) assumes both child subtrees are already valid heaps; going top-down violates that precondition and can leave a non-heap. You must go from down to 0.
"if l < n and A[l] > A[largest] — the l < n check is optional since children always exist."
They don't. Recall , : a node near the bottom may have zero, one, or two children, and those computed indices can exceed . The l < n and r < n guards prevent reading past the array end (an out-of-bounds bug).
"siftDown picks the larger child every time, so a min-heap needs the opposite: pick the larger child."
For a min-heap you compare for the smaller child and swap when a child is smaller than the parent — the whole comparison direction flips (and largest becomes smallest), not just one line.
"Since half the nodes are leaves and do nothing, heapify only touches nodes total."
The leaves are skipped as starting points, but they are still visited as children. Concretely, take [3, 9, 2] (): the only siftDown call is on the root , which computes , and compares against both leaves 9 and 2 even though 9's subtree is a leaf. So the leaves are read via the l<n/r<n checks; the bound counts that work, and the real point is that cost stays constant per node on average, not that half the nodes are ignored.
"A height- node costs because a subtree of height has about nodes."
The cost is , not . siftDown follows a single root-to-leaf path, dropping at most levels — it does not touch the whole subtree.

Why questions

Why do we start at and not at or ?
is the parent of the last element (index ), since parent-of-; every index above it is an internal node, every index after it is a leaf. Off-by-one here either wastes work on a leaf or skips a real internal node.
Why does going bottom-up satisfy siftDown's precondition for free?
The children of node live at indices and , both larger than , so they were already processed earlier in the descending loop. Their subtrees are therefore already valid heaps — exactly what siftDown(i) needs.
Why is the number of height- nodes about ?
Each level up the tree holds roughly half as many nodes as the level below: leaves ≈ , height-1 ≈ , and so on (see the second figure). So height holds ≈ — tall nodes are exponentially scarce.
Why do we differentiate the geometric series to evaluate , and where does the constant come from?
We know in closed form, but we need a -weighted version. Differentiating gives ; multiply by to get . Plugging : . That is the finite constant that makes the whole sum linear.
Why does the "" disappear even though the root's siftDown really is ?
There is only one root (and few near-root nodes). Their cost, multiplied by their tiny count, contributes a bounded amount. The convergent weighted sum absorbs it into a constant.
Why is bottom-up siftDown preferred over siftUp for a one-shot build, but siftUp still matters elsewhere?
For building from a full array, siftDown is asymptotically faster ( vs ). But siftUp is the right tool for incremental insertion into an existing heap, where you add one element at the bottom and float it up — see siftUp vs siftDown.

Edge cases

For , what does bottom-up heapify do?
The start index is , so the loop runs zero times. A single element is trivially a valid heap — correct, and no work needed.
For (empty array), is heapify well-defined?
Yes: the start index is , the loop body never runs, and an empty array vacuously satisfies the heap property. No crash, no swaps.
What happens on an array that is already a valid heap?
siftDown finds largest == i at every node and breaks immediately, doing zero swaps. It still visits each internal node once, so the cost is still comparisons — heapify does not "detect" and short-circuit an already-heaped array to .
Does a node ever need to swap with both children in one step?
No. Each siftDown step swaps with at most one child — the larger of the two (max-heap). It then continues down from that child's position, possibly swapping again, but never two swaps at the same level.
What if all elements are equal?
Every parent already equals (hence ) its children, so siftDown breaks at each node with no swaps. The array is a valid heap unchanged — the comparison uses (strict), so equal children never trigger a swap.

Recall One-line takeaways to re-derive under pressure

Start index ::: , the last internal node. Direction of the loop ::: from that index down to 0 (bottom-up). Child indices of ::: left , right . Cost of a height- node ::: , a single path down. Count at height ::: about . The magic constant ::: , so total .