In a 0-indexed array heap, why are a node's children at 2i+1 and 2i+2 and its parent at (i-1)/2?
answer
- the array is written in level order
- how many nodes precede level L?
- level L starts at index 2^L - 1
- the k-th node parents nodes 2k and 2k+1
- the 2^L terms cancel out
basics
~20 sLevel-order layout forces the arithmetic. Level L starts at index 2^L - 1, and the k-th node of a level parents nodes 2k and 2k+1 of the next; substituting collapses to children 2i+1, 2i+2 and parent floor((i-1)/2).
solid answer
~50 sThe layout writes the tree in level order into slots `0..n-1`, so the arithmetic is forced, not chosen. Level `L` (root at level 0) occupies indices `2^L - 1` through `2^(L+1) - 2`, and node `i` is the `k`-th node on its level where `k = i - (2^L - 1)`. Its children are nodes `2k` and `2k+1` on level `L+1`, which starts at `2^(L+1) - 1`; substituting gives `2^(L+1) - 1 + 2k = 2i + 1` and `2i + 2`. Inverting yields `parent(i) = floor((i-1)/2)`, guarded by `i > 0` since the root has none. The 1-based variant — root at index 1, children `2i` and `2i+1`, parent `floor(i/2)` — is a *different* scheme; mixing the two is the classic silent corruption, because it aims a node at its sibling or aunt instead of its parent and never goes out of bounds.
code
pseudocode · 11 lines// ratings[0..n-1], written in level order
left(i) = 2*i + 1
right(i) = 2*i + 2
parent(i) = floor((i - 1) / 2) // require i > 0
// level L (root = level 0) occupies indices 2^L - 1 .. 2^(L+1) - 2
// node i is the k-th node on its level, k = i - (2^L - 1)
// its children are nodes 2k and 2k+1 of level L+1 -> 2*i + 1, 2*i + 2
...
// 1-based layout, root at index 1, slot 0 unused - NOT interchangeable:
// left(i) = 2*i, right(i) = 2*i + 1, parent(i) = floor(i / 2)go deeper
Memorising children 2i+1 / 2i+2 and parent (i-1)/2 for the 0-indexed layout is the baseline. Be able to apply them to a small array on paper without hesitating.
Derive the formulas from level order rather than reciting them: count the nodes before level L, and show the 2^L terms cancel. Also state the root guard and the child bounds check.
Explain how a 0-based / 1-based mixup corrupts a structure without any crash or out-of-bounds access, and name the cheap O(n) validity assertion that catches it in tests.
Be ready to argue for one convention repo-wide and for encoding it in a single named helper, since the failure mode of the alternative is silent wrong results rather than an exception.
## Where the formulas come from An implicit heap stores no tree nodes at all. It writes the complete tree into a flat array in **level order**: the root, then level 1 left-to-right, then level 2, and so on. Everything else follows from counting. A complete binary tree has `2^L` nodes on level `L` (root = level 0), so levels `0..L-1` together hold `2^L - 1` nodes. Therefore **level `L` begins at index `2^L - 1`** and ends at `2^(L+1) - 2`. Take node at index `i` on level `L`. It is the `k`-th node on that level, counting from zero: ``` k = i - (2^L - 1) ``` In a complete tree, the `k`-th node of a level parents the `2k`-th and `(2k+1)`-th nodes of the next level — each node contributes exactly two children, in order. Level `L+1` begins at `2^(L+1) - 1`, so: ``` left = (2^(L+1) - 1) + 2k = 2^(L+1) - 1 + 2(i - 2^L + 1) = 2^(L+1) - 1 + 2i - 2^(L+1) + 2 = 2i + 1 right = 2i + 2 ``` The `2^L` terms cancel, which is why the rule is uniform across every level and needs no knowledge of `L` at runtime. Inverting `left = 2i + 1` and `right = 2i + 2` gives a single formula for both: `parent(i) = floor((i - 1) / 2)`. ## Boundaries that bite - **The root has no parent.** With true flooring, `floor((0-1)/2) = -1`. With truncation toward zero, the same expression yields `0` and the root becomes its own parent — an infinite loop in any upward walk. Guard with `i > 0` before ever calling `parent`; do not rely on the division's rounding behaviour. - **Children may not exist.** `2i+1` and `2i+2` are valid only when they are `< n`. A node is a leaf exactly when `2i + 1 >= n`. - **The last internal node** is at index `floor(n/2) - 1`; every index from `floor(n/2)` to `n-1` is a leaf. That is roughly half the array, and it is why the leaf count is about `n/2`. - **Overflow.** For very large heaps `2i + 2` can exceed the index type's range before `n` does. Where that matters, compute `left = i + i + 1` on a wide type or bound `n`. ## The 1-based scheme, and the mixup The alternative layout wastes slot 0 and puts the root at index 1. Then level `L` starts at index `2^L`, and the same counting argument gives: ``` left(i) = 2i, right(i) = 2i + 1, parent(i) = floor(i / 2) ``` It is genuinely tidier — the children formulas are a shift and a shift-or-one, and `parent` needs no subtraction — which is why textbook pseudocode often uses it. It is also the source of the most instructive bug in this topic. Suppose a matchmaking service keeps its rating pool 0-indexed, but someone copies `parent(i) = i / 2` out of a textbook. Trace it: index 1 maps to 0, which happens to be correct. Index 2 maps to 1 — but index 2's real parent is `(2-1)/2 = 0`; index 1 is its **sibling**. Index 4 maps to 2, while its real parent is 1; index 2 is its aunt. Every even index gets a wrong ancestor. And nothing crashes: all the indices are in range, the array stays the same size, and every operation returns. The structure simply stops being a heap in places, so occasionally the extracted "lowest-rated waiting player" is not the lowest — a matchmaking bug that surfaces as unfair pairings weeks later, not as a stack trace. The mirror bug is equally quiet: using `2i` and `2i+1` for children in a 0-indexed array makes node 0 its own left child. The defence is a cheap `isHeap` assertion in tests — scan `i` from 0 to `floor(n/2) - 1` and verify the invariant against both in-range children, O(n) — plus picking **one** convention and writing the three formulas in a single place rather than inlining the arithmetic at each use site. ## Why this trick is worth it The payoff is that navigation is arithmetic instead of memory: no child pointers to store, no parent pointer to maintain, no per-node allocation, and no way for the structure to become internally inconsistent through a dangling link. The price is that the tree must stay **complete** — the formulas assume slots `0..n-1` are all occupied — and that elements move as the heap changes, so no stable reference to an element survives an operation unless you maintain a separate index.
- What actually breaks if a 0-indexed heap uses parent(i) = i / 2?Every even index gets the wrong ancestor — index 2 is aimed at index 1, its sibling, and index 4 at index 2, its aunt. Nothing goes out of bounds and nothing throws, so the structure silently stops satisfying the invariant in places and occasionally yields a non-extreme element. It is caught by an O(n) heap-validity assertion in tests, not by a crash.
- Given n elements, which indices are leaves?Indices floor(n/2) through n-1 — a node at index i is a leaf exactly when 2i + 1 >= n. That means the last internal node is at floor(n/2) - 1, and roughly half of all slots are leaves, which is why leaf-only scans are still O(n).
- How would the index formulas change for a d-ary heap?Node i's children become d*i + 1 through d*i + d, and parent(i) = floor((i - 1) / d). The tree gets shallower — height about log_d(n) — so upward walks shorten while each downward step must compare d children instead of two, trading fewer levels for more comparisons per level.
saying these in an interview costs you the question
- Mixes 1-based children 2i with 0-based parent (i-1)/2
- Calls parent on the root and loops forever
- Forgets to bounds-check 2i+1 and 2i+2 against n
- Claims the formulas need the node's level at runtime
- Says the layout requires storing child pointers too