PATTERN 1 — TRAVERSAL#
1.1 Pattern Breakdown#
Traversal is not one technique. It is a family united by a single idea: impose a total order on the nodes, then visit each one exactly once. The sub-variants differ in what imposes the order and what state rides along with the visit.
| # | Sub-variant | Order imposed by | State carried | Space |
|---|---|---|---|---|
| A | Recursive DFS — the three orders | the call stack | the implicit root-path | O(h) |
| B | Iterative DFS — explicit stack | a stack you own | whatever you push | O(h) |
| C | BFS — level order | a FIFO queue + a size snapshot | the current frontier | O(w) |
| D | Level-order derivatives | as C, then a per-level rule | one accumulator per level | O(w) |
| E | Coordinate-indexed traversal | a computed (row, col) or heap index | an index per node | O(n) |
| F | Tree as a graph | a parent map + BFS from any node | visited — trees stop being acyclic-by-direction | O(n) |
| G | Morris threading | temporary right-child threads | none | O(1) |
| H | N-ary / generalized children | a child list instead of left/right | as A–D | O(h) or O(w) |
Sub-variants worth stating explicitly:
- B is not "A without recursion." The iterative inorder and postorder machines have genuinely different shapes, and postorder-by-reversed-preorder is the trick people fail to reconstruct under pressure.
- C hinges on one line —
int sz = q.size();taken before the inner loop. Every level-order problem is that snapshot plus an accumulator. - E is the family where the node's position is data. Heap indexing (
2i,2i+1) for width,(col, row)sort keys for vertical order. - F is the sub-variant most people never learn: once a question asks about distance in any direction, the tree is a graph, and top-down DFS cannot answer it at all.
- G exists for exactly one interview sentence: "can you do it in O(1) space?"
1.2 Problem Table#
A Recursive DFS: the three orders#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 1★ | 144. Binary Tree Preorder Traversal | Easy | A | The atom. Establishes the base case if (node == null) return; and the fact that "visit" is one line you can move. | |
| 2★ | 94. Binary Tree Inorder Traversal | Easy | A | Moving the visit line one position changes the entire output order. This is the whole lesson of sub-variant A. | |
| 3★ | 145. Binary Tree Postorder Traversal | Easy | A | Postorder is the only order in which a node is processed after both children — therefore the only order that can aggregate from below. Everything in Pattern 2 is postorder. | |
| 4○ | 589. N-ary Tree Preorder Traversal | Easy | A + H | Same recursion with a child list. Do it only if the generalization isn't obvious. |
B Iterative DFS with an explicit stack#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 5★ | 94. Binary Tree Inorder Traversal (iterative) | Easy | B | The follow-up is the actual exercise. "Descend left pushing, pop, visit, go right" — the only DFS machine you must be able to write without recursion. | |
| 6★ | 145. Binary Tree Postorder Traversal (iterative) | Easy | B | Reverse-preorder: push left before right, then reverse the output. Know that this is not true postorder timing — it produces the right list, not the right visit moments. | |
| 7★ | 173. Binary Search Tree Iterator | Medium | B | A paused inorder. Forces you to store the machine's state between calls, which proves you understand what the stack held. O(h) space, amortized O(1) next(). | |
| 8○ | 331. Verify Preorder Serialization of a Binary Tree | Medium | B | Slot counting — a traversal with no tree at all. Elegant, occasionally asked, teaches nothing structural. |
C BFS: level order with the size snapshot#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 9★ | 102. Binary Tree Level Order Traversal | Medium | C | The size snapshot. int sz = q.size() before the inner loop is the one line that separates levels; without it you have a flat traversal. | |
| 10⚠︎ | 111. Minimum Depth of Binary Tree | Easy | C | The obvious 1 + min(left, right) is wrong: a node with one child would report depth 1 through a null that is not a leaf. Also the first problem where BFS strictly beats DFS — it stops at the first leaf. | |
| 11○ | 107. Binary Tree Level Order Traversal II | Medium | C | #9 with addFirst. Two-minute warm-up, nothing new. |
D Level-order derivatives#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 12★ | 199. Binary Tree Right Side View | Medium | D | "Last node of each level." Teaches that the accumulator can be a single value, and that the DFS solution (visit right first, record on first arrival at a new depth) is equally valid — write both. | |
| 13★ | 103. Binary Tree Zigzag Level Order Traversal | Medium | D | The direction flag belongs to the output list, not the queue. Reversing the queue is the standard wrong turn. | |
| 14○ | 637. Average of Levels in Binary Tree | Easy | D | Pure rep. Watch the long accumulator. | |
| 15○ | 515. Find Largest Value in Each Tree Row | Medium | D | Pure rep. | |
| 16○ | 1161. Maximum Level Sum of a Binary Tree | Medium | D | Rep with a 1-indexed answer; the off-by-one is the only content. |
E Coordinate-indexed traversal#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 17★ | 662. Maximum Width of Binary Tree | Medium | E | Heap indexing: left = 2i, right = 2i + 1. Width is an index difference, not a node count. Normalize each level against its first index or the indices overflow long on a 3000-deep skew. | |
| 18⚠︎ | 987. Vertical Order Traversal of a Binary Tree | Hard | E | The trap: traversal order is not output order. Nodes at the same (row, col) must be sorted by value, which no BFS or DFS gives you for free. Collect (col, row, val) triples, then sort. | |
| 19○ | 314. Binary Tree Vertical Order Traversal PRO | Medium | E | The easy version of #18 — no value tie-break. Free substitute: solve 987 and drop the third sort key. |
F Tree as a graph#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 20⚠︎ | 863. All Nodes Distance K in Binary Tree | Medium | F | The highest-value problem in Pattern 1. No top-down DFS can answer it: distance runs upward too. Build a parent map, then BFS from the target with a visited set. The moment a tree question mentions distance in any direction, it is a graph question. | |
| 21★ | 2385. Amount of Time for Binary Tree to Be Infected | Medium | F | Identical machine, different question — the answer is the number of BFS rounds. This is the transfer rep; if #20 was memorized rather than understood, this exposes it. | |
| 22○ | 742. Closest Leaf in a Binary Tree PRO | Medium | F | Same parent-map BFS with a leaf predicate. Free substitute: 863. |
G Morris traversal#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 23★ | 94. Binary Tree Inorder Traversal (Morris, O(1) space) | Easy | G | Exists for one interview sentence: "now do it in constant space." Thread the inorder predecessor's right pointer to the current node, then undo the thread on the second visit. The undo is the whole problem. |
H N-ary and generalized children#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 24★ | 429. N-ary Tree Level Order Traversal | Medium | H | BFS where the frontier expansion is a loop, not two lines. Confirms that C generalizes without change. | |
| 25★ | 559. Maximum Depth of N-ary Tree | Easy | H | Aggregation over a child list: the identity element for max over zero children is 0, and getting that wrong is the only bug available. | |
| 26○ | 590. N-ary Tree Postorder Traversal | Easy | H | Redundant after #24 and #3. |
Extra Reps — Traversal (only if a gate fails)#
| Solved | Problem | Targets |
|---|---|---|
| 993. Cousins in Binary Tree | Depth and parent recorded in one pass. | |
| 1302. Deepest Leaves Sum | Level accumulator with a reset. | |
| 623. Add One Row to Tree | Level-order with structural insertion mid-traversal. | |
| 1609. Even Odd Tree | Per-level monotonicity + parity, four failure conditions in one loop. | |
| 671. Second Minimum Node In a Binary Tree | Pruned DFS — stop descending when the invariant says you cannot improve. | |
| 366. Find Leaves of Binary Tree PRO | Height-indexed bucketing. Free substitute: 1302 plus a height computation. |
1.3 Templates#
A Recursive DFS, all three orders#
One skeleton, one line moved. Where you put the visit relative to the two recursive calls is the traversal order — there is no third idea here.
Mental model
“dfs(node) is a promise: I will completely visit this subtree and touch nothing outside it. And the call stack, without my doing anything, is the path from the root to where I am.”
Traversal is not one technique. It is a family united by a single idea — impose a total order on the nodes, then visit each one exactly once — and the sub-variants differ in what imposes the order and what state rides along.
Of the three orders, postorder is load-bearing for everything that follows. It is the only one where both recursive calls have already returned by the time the parent's visit runs, which is exactly what computing a subtree aggregate requires.
null is a legal subtree — the empty one. Guarding the callee means exactly one null check exists; guarding the caller doubles the branching and hides bugs as the function grows.
Recognition — reach for this when
- You need to visit every node once, and
O(h)stack space is acceptable. - The work at a node depends on its subtree (postorder) or on the path down to it (preorder).
- You want the root-path for free — the call stack already is it.
- But not when the answer is level-by-level. That needs a queue, which is sub-variant C.
- And not under an
O(1)space constraint — that is Morris, sub-variant G.
JavaINVARIANT: dfs(node) completely visits the subtree rooted at node and touches nothing12 lines
// INVARIANT: dfs(node) completely visits the subtree rooted at node and touches nothing
// outside it. The call stack IS the path root → node.
// BASE CASE: null is a legal subtree — the empty one. Guard the CALLEE, never the caller:
// "if (node.left != null) dfs(node.left)" doubles the branching and hides bugs.
void dfs(TreeNode node, List<Integer> out) {
if (node == null) return;
// out.add(node.val); // PREORDER — before both children
dfs(node.left, out);
out.add(node.val); // INORDER — between the two children
dfs(node.right, out);
// out.add(node.val); // POSTORDER — after both children
}Why it works — the invariant, and why postorder is the special one4 steps
Four lines that the whole of Pattern 2 rests on. The last one is the reason this sub-variant comes first.
- 1
The invariant.
dfs(node)completely visits the subtree rooted atnodeand touches nothing outside it. That containment is what lets you reason about one node at a time. - 2
The stack is the path. The chain of active calls is exactly root -> node. Any problem about the path from the root needs no extra data structure — it is already on the stack.
- 3
The base case.
nullis the empty subtree, a perfectly legal input. So the guard belongs at the top of the callee: one check, one place. - 4
Why postorder computes aggregates. At the moment a postorder visit runs, both
dfscalls have returned, so both subtree answers already exist. In pre- and inorder at least one child is still unknown when the parent acts — so no aggregate is available to combine.
Three orders, one skeleton, one moved line: if you cannot state why postorder is the only order that can compute a subtree aggregate, stop here — the whole of Pattern 2 depends on it.
Recursive DFS tolerates a null root; BFS never does. The if (node == null) return; guard absorbs the empty tree for free, whereas a queue template dereferences root immediately and needs an explicit guard. Worth knowing as an asymmetry rather than memorising per template.
Space is O(h), which is O(n) on a degenerate tree. A linked-list-shaped tree will overflow the stack, and that is the honest answer when asked about the worst case.
Walkthrough — all three orders from one skeleton8 steps
The same five-node tree, traced once. Each row is a moment during the single walk; the three columns show which order would emit at that moment.
| # | Moment | Preorder | Inorder | Postorder |
|---|---|---|---|---|
| 1 | enter 1 | 1 | ||
| 2 | enter 2 | 2 | ||
| 3 | enter and leave 4 (a leaf) | 4 | 4 | 4 |
| 4 | back at 2, left child done | 2 | ||
| 5 | enter and leave 5 (a leaf) | 5 | 5 | 5 |
| 6 | leave 2 — both children done | 2 | ||
| 7 | back at 1, left subtree done | 1 | ||
| 8 | enter and leave 3, then leave 1 | 3 | 3 | 3, then 1 |
Preorder 1 2 4 5 3, inorder 4 2 5 1 3, postorder 4 5 2 3 1. Row 6 is the one to look at: node 2 is emitted in postorder only after both 4 and 5 are finished — which is precisely why a postorder visit can combine their answers and a preorder visit cannot.
Key observations — what interviewers are listening for4 points
- Say the postorder sentence before you are asked. Both recursive calls have returned, so both subtree answers exist. That single line is the A gate, and it is the foundation of every problem in Pattern 2.
- Guard the callee, always. One
if (node == null) return;at the top. Guarding at the call site doubles the branching, and the duplicated logic is where base-case bugs hide once the function grows. - The call stack is free state. Root-path problems need no auxiliary structure in recursive DFS. Recognising that is what makes sub-variants E and F of Pattern 2 feel natural later.
- Name the space honestly.
O(h), degrading toO(n)on a degenerate tree. SayingO(log n)without the caveat assumes balance you were never promised.
Common mistakes4 traps
-
Guarding the caller:
if (n.left != null) dfs(n.left)Symptom: duplicated null logic, and missed base cases as the function grows.
Prevention: Guard the callee: one
if (node == null) return;at the top, always. -
Trying to compute a subtree aggregate in preorder
Symptom: the parent acts before its children are known, so the aggregate is wrong or incomplete.
Prevention: Aggregates are postorder. If the parent needs the children's answers, the visit goes after both calls.
-
Assuming
O(log n)stack spaceSymptom: stack overflow on a degenerate, list-shaped tree.
Prevention: Space is
O(h). Only a balanced tree makes thatO(log n). -
Rewriting the skeleton per order
Symptom: three near-identical functions and three chances to introduce a bug.
Prevention: One skeleton, one moved line. The orders differ by placement, nothing else.
Key takeaway
- Trigger: visit every node once; work depends on the subtree or the root-path.
- The skeleton: guard the callee, recurse left, recurse right — and place the visit to pick the order.
- Postorder is special: both calls have returned, so both subtree answers exist.
- Cost:
O(n)time,O(h)space —O(n)on a degenerate tree. - Gate: all three orders from one skeleton, plus the one-sentence reason postorder owns aggregates. See §5.1.
B Iterative DFS with an explicit stack#
This is not A without recursion. You are hand-building the machine the call stack was running for you — and inorder and postorder turn out to have genuinely different shapes.
Mental model
“The stack holds the ancestors I still owe something to. For inorder that is exactly the ancestors whose left subtree is finished but whose own value I have not emitted yet.”
Postorder-by-reversed-preorder is the piece people fail to reconstruct under pressure, and it comes with a caveat that matters more than the trick itself.
LC 173 is the reason this sub-variant earns its place: a paused inorder. The same machine, stopped between steps, with the stack as the saved position.
addFirst does the reversing as you go, with no second pass. Pushing left before right is what makes right pop first, which is what makes the reversal come out as postorder.
Recognition — reach for this when
- Recursion is unavailable, or the stack depth would overflow.
- You need to pause the traversal between nodes — an iterator, a merge of two trees.
- You want explicit control over what is remembered at each node.
- But not when you need to act on a node strictly after both children. Reversed-preorder gives the right list at the wrong time.
JavaINORDER, iterative.15 lines
// INORDER, iterative.
// INVARIANT: the stack holds exactly those ancestors of `cur` whose left subtree is finished
// and whose own value has not yet been emitted.
List<Integer> inorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> st = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !st.isEmpty()) {
while (cur != null) { st.push(cur); cur = cur.left; } // descend left, pushing
cur = st.pop();
out.add(cur.val); // its left subtree is done
cur = cur.right; // now we owe the right subtree
}
return out;
}JavaPOSTORDER, iterative — by reversed preorder.16 lines
// POSTORDER, iterative — by reversed preorder.
// Root-Right-Left, emitted front-first, is Left-Right-Root.
// CAVEAT: this yields the correct LIST, not the correct visit MOMENTS. If you must act on a
// node strictly after both children (freeing, folding), use the lastVisited form.
List<Integer> postorder(TreeNode root) {
LinkedList<Integer> out = new LinkedList<>();
Deque<TreeNode> st = new ArrayDeque<>();
if (root != null) st.push(root);
while (!st.isEmpty()) {
TreeNode n = st.pop();
out.addFirst(n.val); // push-front == reverse, without a second pass
if (n.left != null) st.push(n.left); // LEFT pushed first, so RIGHT pops first
if (n.right != null) st.push(n.right);
}
return out;
}Java173. Binary Search Tree Iterator — a PAUSED inorder.10 lines
// 173. Binary Search Tree Iterator — a PAUSED inorder.
// The stack always holds the not-yet-returned ancestors, smallest on top.
// Amortized O(1) per next() (each node is pushed and popped exactly once), O(h) space.
class BSTIterator {
private final Deque<TreeNode> st = new ArrayDeque<>();
BSTIterator(TreeNode root) { pushLeft(root); }
private void pushLeft(TreeNode n) { while (n != null) { st.push(n); n = n.left; } }
public boolean hasNext() { return !st.isEmpty(); }
public int next() { TreeNode n = st.pop(); pushLeft(n.right); return n.val; }
}Why it works — the inorder invariant, and the postorder trick's limit4 steps
The inorder machine is worth deriving once; the postorder one is worth knowing precisely what it does and does not give you.
- 1
The inorder invariant. The stack holds exactly those ancestors whose left subtree is finished and whose own value has not yet been emitted. Everything the loop does maintains that sentence.
- 2
The two halves of the loop. Descend left while pushing — you are deferring every node you pass. Then pop, emit (its left subtree is now provably done), and move to its right child, because that is the only debt left.
- 3
The postorder trick. Root-Right-Left, emitted front-first, reads as Left-Right-Root. Push left first so that right pops first, and use
addFirstso the reversal happens without a second pass. - 4
What the trick does not give you. It produces the correct sequence, but each node is processed before its children. If the visit has a side effect that must happen after both children — freeing, folding, releasing — the timing is wrong even though the list is right.
The caveat that makes reversed-preorder a list trick rather than a traversal: it yields the correct list, not the correct visit moments. If you must act on a node strictly after both children (freeing, folding), use the lastVisited form.
LC 173's next() is amortized O(1), and the gate asks why. Each node is pushed exactly once and popped exactly once across the whole iteration, so n calls do O(n) total work even though a single call may descend a long left spine. Space is O(h).
Deque is both a stack and a queue, which is the hazard. push/pop give LIFO, add/poll give FIFO. Mixing them inside one method silently turns a BFS into a DFS.
Walkthrough — iterative inorder on the five-node tree6 steps
Watch the stack. At every emit, the invariant holds: everything on the stack is an ancestor whose left subtree is finished and whose value is still owed.
| # | cur | Stack (top first) | Action | Emitted so far |
|---|---|---|---|---|
| 1 | 1 -> 2 -> 4 | 4, 2, 1 | descend left, pushing each | -- |
| 2 | -- | 2, 1 | pop 4, emit, go to its right (null) | 4 |
| 3 | -- | 1 | pop 2, emit, go to its right -> 5 | 4, 2 |
| 4 | 5 | 5, 1 | descend from 5 (no left child) | 4, 2 |
| 5 | -- | 1 | pop 5, emit, right is null | 4, 2, 5 |
| 6 | -- | (empty) | pop 1, emit, go right -> 3, push and pop it | 4, 2, 5, 1, 3 |
Inorder 4 2 5 1 3, matching the recursive walk exactly. Note step 3: node 2 is emitted the moment it is popped, and only then does its right subtree get explored — which is the iterative restatement of the visit sits between the two calls.
Key observations — what interviewers are listening for4 points
- State the stack invariant, not the loop. Ancestors whose left subtree is done and whose value is still owed. The gate asks for it, and it reconstructs the code if you blank.
- The amortization argument is short. Each node is pushed once and popped once, so
ncalls costO(n)overall. That is the whole answer to why isnext()amortizedO(1)? - Know the postorder caveat before you use the trick. Right list, wrong moments. Volunteering that distinction shows you understand what a traversal order actually is.
- An iterator is a paused traversal. Framing LC 173 that way — rather than as a new problem — is what makes it a two-minute problem instead of a twenty-minute one.
Common mistakes4 traps
-
Using reversed-preorder postorder for side effects
Symptom: a node is processed before its children are done, so folds and frees happen in the wrong order.
Prevention: It produces the right list, not the right timing. Use recursion or the
lastVisitedform. -
Mixing
push/popwithadd/pollon oneDequeSymptom: a silent DFS where you intended a BFS, or vice versa.
Prevention:
add/pollfor FIFO,push/popfor LIFO. Never mix them in one method. -
Pushing right before left in the postorder trick
Symptom: the reversal comes out as the wrong order entirely.
Prevention: Push left first so right pops first — that ordering is what the reversal depends on.
-
Emitting during the descend loop
Symptom: you get preorder while believing you wrote inorder.
Prevention: The descend loop only pushes. Emission happens after the pop.
Key takeaway
- Trigger: recursion unavailable, stack depth a risk, or the traversal must be pausable.
- Inorder invariant: the stack holds ancestors whose left subtree is done and whose value is still owed.
- Postorder trick: Root-Right-Left with
addFirst; push left first so right pops first. - The caveat: correct list, wrong moments — use
lastVisitedwhen timing matters. - Gate: iterative inorder blind with the invariant stated, then LC 173 and why
next()is amortizedO(1). See §5.1.
C BFS, level order#
One line makes level order work: snapshot q.size() before the inner loop. Everything else in this sub-variant is that snapshot plus an accumulator.
Mental model
“The queue holds exactly one frontier at a time. If I write down how big it is before I start draining, I know precisely where the level ends — even though I am appending the next level while I go.”
The subtlety is that the queue is being modified while you consume it. q.size() is a moving target, so reading it inside the loop condition merges every level into one flat list.
There is also a boundary asymmetry worth internalising: recursive DFS tolerates a null root; BFS never does, because the template dereferences root on the very first q.add.
Read q.size() inside the condition and the loop keeps consuming the children it just added, so every level runs into the next one.
Recognition — reach for this when
- The answer is organised by level — per-level lists, averages, maxima, the last node.
- You want the shallowest answer and can stop early — BFS reaches it first.
- Width matters more than depth, and
O(w)space is acceptable. - But not when the work depends on a subtree aggregate. That is postorder recursion, sub-variant A.
JavaTHE ONE LINE: sz is snapshotted BEFORE the inner loop. Without it the queue grows while21 lines
// THE ONE LINE: sz is snapshotted BEFORE the inner loop. Without it the queue grows while
// you drain it and every level boundary is lost.
// BOUNDARY: BFS templates need an explicit null-root guard; recursive DFS templates do not.
List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> q = new ArrayDeque<>();
q.add(root);
while (!q.isEmpty()) {
int sz = q.size();
List<Integer> level = new ArrayList<>(sz);
for (int i = 0; i < sz; i++) {
TreeNode n = q.poll();
level.add(n.val);
if (n.left != null) q.add(n.left);
if (n.right != null) q.add(n.right);
}
out.add(level);
}
return out;
}Java111. Minimum depth — the recursion everyone writes first is WRONG:28 lines
// 111. Minimum depth — the recursion everyone writes first is WRONG:
// return 1 + Math.min(depth(left), depth(right));
// A node with one child returns 1, because the ABSENT child returns 0 and wins the min.
// A leaf is "no children", not "one null child".
int minDepth(TreeNode root) {
if (root == null) return 0;
if (root.left == null) return 1 + minDepth(root.right); // one real child: no min at all
if (root.right == null) return 1 + minDepth(root.left);
return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}
// BFS is strictly better here: it returns at the FIRST leaf instead of exploring everything.
int minDepthBfs(TreeNode root) {
if (root == null) return 0;
Deque<TreeNode> q = new ArrayDeque<>();
q.add(root);
int depth = 1;
while (!q.isEmpty()) {
for (int i = q.size(); i > 0; i--) {
TreeNode n = q.poll();
if (n.left == null && n.right == null) return depth; // first leaf wins
if (n.left != null) q.add(n.left);
if (n.right != null) q.add(n.right);
}
depth++;
}
return depth;
}Why it works — the size snapshot, and the one-child trap4 steps
The skeleton is four lines and one of them carries the whole idea. The second half of this section is the classic wrong recursion that BFS fixes outright.
- 1
The queue holds one frontier. At the top of the outer loop, everything in the queue is at the same depth. That is the property being maintained.
- 2
Draining while enqueueing breaks it. Each
pollremoves a node of the current level and eachaddappends a node of the next one, soq.size()changes meaning mid-loop. - 3
The snapshot freezes the boundary.
int sz = q.size();captures the level's width before any of it is consumed, so the inner loop runs exactly over this level and stops. - 4
The null-root guard. BFS dereferences
rootimmediately by enqueuing it. Recursive DFS absorbs a null root in its base case; BFS must check explicitly.
The one line: int sz = q.size(); taken before the inner loop, never inside its condition. Without it the queue grows while you drain it and every level boundary is lost.
LC 111 is the trap that makes this sub-variant matter. The recursion everyone writes first — return 1 + Math.min(depth(left), depth(right)); — is wrong on every one-child node, because the absent child returns 0 and wins the min. A leaf is no children, not one null child, so the one-child case has to be handled explicitly.
BFS is strictly better for minimum depth. It returns at the first leaf it meets instead of exploring the entire tree, and it makes the one-child trap impossible to write.
Walkthrough — level order, watching the snapshot3 steps
The sz column is the whole lesson: it is read once per level, and the queue is longer than sz by the time the inner loop ends.
| # | Queue at level start | sz | Drained | Enqueued during | Level output |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 2, 3 | [1] |
| 2 | 2, 3 | 2 | 2, 3 | 4, 5 | [2, 3] |
| 3 | 4, 5 | 2 | 4, 5 | nothing | [4, 5] |
Result [[1], [2,3], [4,5]]. Look at row 2: sz was 2, but by the time the inner loop finished the queue held 2 more nodes. Had the condition read i < q.size(), the loop would have kept going straight into level three and produced [[1], [2,3,4,5]].
Key observations — what interviewers are listening for4 points
- The snapshot is the sub-variant. Every level-order derivative in D is this skeleton plus an accumulator. Getting the snapshot reflexive pays for eight problems.
- Know the null-root asymmetry. DFS templates tolerate it, BFS templates do not. It is a one-line fix and a guaranteed null-pointer exception if you forget it.
- For shallowest-anything, BFS beats recursion on merit. Not just stylistically — it terminates at the first hit rather than exploring the whole tree, and it sidesteps the one-child trap by construction.
- Space is
O(w), notO(h). The widest level dominates. On a complete tree that isn/2, which is worth saying when comparing against DFS'sO(h).
Common mistakes4 traps
-
BFS without the
q.size()snapshotSymptom: levels merge into one flat list.
Prevention: Take
int sz = q.size();before the inner loop, never inside its condition. -
1 + min(left, right)for minimum depthSymptom: wrong on every one-child node — the absent child returns 0 and wins the min.
Prevention: A leaf is both children null. Handle the one-child case explicitly, or use BFS and avoid it.
-
Missing the null-root guard
Symptom:
NullPointerExceptionon the firstq.add(root)dereference.Prevention: Recursive DFS tolerates a null root; BFS never does. Guard explicitly.
-
Using
Dequeas a queue withpushSymptom: silent DFS instead of BFS — the output looks plausible and is wrong.
Prevention:
add/pollfor FIFO.push/popis LIFO and belongs in sub-variant B.
Key takeaway
- Trigger: the answer is per-level, or you want the shallowest result and can stop early.
- The one line:
int sz = q.size();before the inner loop. - Guard: BFS needs an explicit null-root check; recursive DFS does not.
- LC 111:
1 + min(...)is wrong on one-child nodes; BFS returns at the first leaf. - Gate: the skeleton blind with the snapshot and the guard, plus why LC 111 breaks the naive recursion. See §5.1.
D Level-order derivatives#
Once you own the level skeleton, every derivative is one per-level rule: keep the last one, alternate the direction, average them, take the maximum.
Mental model
“Same machine, different accumulator. The only question left is what I do with a level once I am holding all of it.”
The interesting cases are the ones where a DFS does the same job more cheaply. LC 199's right side view is the example worth knowing both ways, because the two forms have different space costs and the gate asks you to name which is which.
Zigzag carries the one real trap in this sub-variant, and it is a trap about where the reversal lives.
In the DFS form of 199, depth == out.size() is the test for first arrival at a new depth — and visiting right before left is what makes the first arrival the rightmost node.
Recognition — reach for this when
- The answer is one value per level — the last, the largest, the average, the sum.
- Or the levels themselves need reshaping, as in zigzag.
- You can state whether you want
O(w)BFS orO(h)DFS, and why. - But not if the rule needs information from a different level. Cross-level dependencies are a recursion problem, not a level-order one.
Java199. Right side view, DFS form: visit RIGHT first, record the first node seen at each depth.8 lines
// 199. Right side view, DFS form: visit RIGHT first, record the first node seen at each depth.
// O(h) space instead of BFS's O(w), and two lines shorter. Know both.
void rightView(TreeNode n, int depth, List<Integer> out) {
if (n == null) return;
if (depth == out.size()) out.add(n.val); // first arrival at a new depth
rightView(n.right, depth + 1, out); // RIGHT before LEFT — that is the whole trick
rightView(n.left, depth + 1, out);
}Java103. Zigzag. The direction flag belongs to the OUTPUT list, never to the queue:23 lines
// 103. Zigzag. The direction flag belongs to the OUTPUT list, never to the queue:
// reversing the queue also reverses the children's enqueue order and corrupts
// every level below it.
List<List<Integer>> zigzag(TreeNode root) {
List<List<Integer>> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> q = new ArrayDeque<>();
q.add(root);
boolean l2r = true;
while (!q.isEmpty()) {
int sz = q.size();
LinkedList<Integer> level = new LinkedList<>();
for (int i = 0; i < sz; i++) {
TreeNode n = q.poll();
if (l2r) level.addLast(n.val); else level.addFirst(n.val);
if (n.left != null) q.add(n.left);
if (n.right != null) q.add(n.right);
}
out.add(level);
l2r = !l2r;
}
return out;
}Why it works — the two forms of 199, and why zigzag must reverse the output4 steps
One derivative that is better as a DFS, and one that punishes the obvious implementation. Together they are the whole sub-variant.
- 1
The skeleton does not change. Sub-variant C's loop, with the snapshot, is still underneath. Only the accumulator differs.
- 2
199 as a DFS. Visit right before left and record a node whenever
depth == out.size(). The first node reached at any new depth is therefore the rightmost one, and the space cost isO(h)rather thanO(w). - 3
Zigzag's tempting mistake. Reversing the frontier looks like it produces alternating output, and it does — for one level.
- 4
Why it then breaks. Reversing the queue also reverses the order in which those nodes enqueue their children, so every level below inherits a scrambled ordering. The output is correct at level one and corrupt underneath.
The rule that keeps zigzag correct below level one: the direction flag belongs to the output list, never to the queue. Reverse the output; leave the frontier alone.
The 199 space trade-off is the gate. BFS costs O(w) — up to n/2 on a complete tree. The right-first DFS costs O(h). Being able to write both and say which is which is what is being tested.
LinkedList gives you addFirst for free, which is why zigzag needs no explicit reversal pass — you simply choose which end to append to.
Walkthrough — zigzag on the five-node tree3 steps
The queue is drained left-to-right on every level. Only the side you append to changes.
| # | Level drained (always L to R) | Direction | Appends | Level output |
|---|---|---|---|---|
| 1 | 1 | left to right | addLast(1) | [1] |
| 2 | 2, 3 | right to left | addFirst(2), then addFirst(3) | [3, 2] |
| 3 | 4, 5 | left to right | addLast(4), addLast(5) | [4, 5] |
Result [[1], [3,2], [4,5]]. The queue order never changed — nodes 2 and 3 still enqueued their children in the normal order, which is why level three comes out as [4, 5] and not reversed. Flip the queue instead and level three would have been corrupted by level two's reversal.
Key observations — what interviewers are listening for4 points
- Know both forms of 199 and their costs.
O(w)BFS versusO(h)right-first DFS. The gate asks for both, and the space comparison is the point of asking. depth == out.size()is a neat first-arrival test. It works because the output list is built strictly in depth order — worth recognising, since the same idiom reappears in other first-arrival problems.- Reverse the output, never the frontier. One sentence that prevents the only real bug in this sub-variant.
- Most derivatives are one line. Largest per level, average per level, last per level — all the same skeleton. Recognising that keeps four problems from feeling like four problems.
Common mistakes4 traps
-
Implementing zigzag by reversing the queue
Symptom: level one is correct and everything below it is corrupt.
Prevention: Reverse the output list. The frontier's order determines the next level's enqueue order.
-
Visiting left before right in the DFS form of 199
Symptom: you record the leftmost node at each depth — the left side view.
Prevention: Right before left. That ordering is the entire trick.
-
Recording with
depth > out.size()Symptom: off-by-one; depths get skipped or duplicated.
Prevention:
depth == out.size()is exactly first arrival at a new depth. -
Using BFS reflexively when
O(h)is availableSymptom:
O(w)space whereO(h)would do — up ton/2versuslog non a balanced tree.Prevention: Ask which dimension you are paying for before choosing the traversal.
Key takeaway
- Trigger: one value per level, or a per-level reshaping.
- The rule: sub-variant C's skeleton plus one accumulator.
- 199: BFS last-of-level is
O(w); right-first DFS withdepth == out.size()isO(h). - Zigzag: flip the output, never the queue.
- Gate: write 199 both ways and say which is
O(h)and which isO(w). See §5.1.
E Coordinate-indexed traversal#
Give every node a coordinate and the problem stops being about tree shape. Width becomes an index subtraction; vertical order becomes a sort.
Mental model
“The node's position is data. Once I attach a (row, col) pair or a heap index, the answer is arithmetic on coordinates rather than a walk over structure.”
Two problems, two different coordinate systems, and each has its own trap. Heap indexing (left = 2i, right = 2i+1) turns width into a difference. (row, col) labelling turns vertical order into a sort — and traversal order stops being output order entirely.
Both traps are about the coordinate rather than the traversal, which is what makes this a sub-variant of its own rather than a footnote under BFS.
Width counts gaps, so a level holding two nodes at indices 0 and 3 has width 4, not 2. That is the whole of LC 662.
Recognition — reach for this when
- The answer depends on where a node sits, not just on its subtree.
- Words like width, vertical, column, diagonal, or a position in a complete tree.
- Gaps matter — absent nodes still occupy space in the answer.
- But not when position is incidental. If only structure matters, coordinates are extra bookkeeping for nothing.
Java662. Width. Heap indexing: left = 2i, right = 2i + 1. Width is an index DIFFERENCE,24 lines
// 662. Width. Heap indexing: left = 2i, right = 2i + 1. Width is an index DIFFERENCE,
// not a node count — the missing nodes between the ends are part of the width.
// NORMALIZE against each level's first index, and carry the index as a long: a deep
// alternating tree makes raw indices exceed any fixed width.
int widthOfBinaryTree(TreeNode root) {
if (root == null) return 0;
int best = 0;
Deque<TreeNode> qn = new ArrayDeque<>();
Deque<Long> qi = new ArrayDeque<>();
qn.add(root); qi.add(0L);
while (!qn.isEmpty()) {
int sz = qn.size();
long first = qi.peek(), last = 0;
for (int k = 0; k < sz; k++) {
TreeNode n = qn.poll();
long i = qi.poll() - first; // renumber this level from 0
last = i;
if (n.left != null) { qn.add(n.left); qi.add(2 * i); }
if (n.right != null) { qn.add(n.right); qi.add(2 * i + 1); }
}
best = Math.max(best, (int) (last + 1));
}
return best;
}Java987. Vertical order. Traversal order is NOT output order.22 lines
// 987. Vertical order. Traversal order is NOT output order.
// Sort key: (col, row, val). The third component is the trap — no traversal produces it.
List<List<Integer>> verticalTraversal(TreeNode root) {
List<int[]> nodes = new ArrayList<>();
collect(root, 0, 0, nodes);
nodes.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
: a[1] != b[1] ? Integer.compare(a[1], b[1])
: Integer.compare(a[2], b[2]));
List<List<Integer>> out = new ArrayList<>();
for (int k = 0; k < nodes.size(); k++) {
if (k == 0 || nodes.get(k)[0] != nodes.get(k - 1)[0]) out.add(new ArrayList<>());
out.get(out.size() - 1).add(nodes.get(k)[2]);
}
return out;
}
void collect(TreeNode n, int row, int col, List<int[]> acc) {
if (n == null) return;
acc.add(new int[]{col, row, n.val});
collect(n.left, row + 1, col - 1, acc);
collect(n.right, row + 1, col + 1, acc);
}Why it works — two coordinate systems, two traps4 steps
Each problem here is easy once the coordinate is chosen and quietly wrong if the trap is missed.
- 1
Heap indexing. Assign the root index 0, then
left = 2iandright = 2i + 1. This is the same numbering an array-backed heap uses, and it encodes horizontal position exactly. - 2
Width is a difference, not a count. Under that numbering the width of a level is
last - first + 1. Counting nodes instead undercounts every sparse level, because the gaps between the ends are part of the width. - 3
Normalisation is mandatory. Raw indices double each level, so a deep alternating tree overflows — first
int, then evenlong. Subtract the level's first index as you go, and carry the index aslong. - 4
LC 987's third sort key. Sorting by
(col, row)alone leaves ties among nodes at the same position, and no traversal resolves them for you. The tiebreak is the node's value, and it has to be added explicitly.
One trap per problem: width is an index difference, so normalise against each level's first index and carry indices as long. And in LC 987 the third sort key is the node's value — nothing gives it to you for free.
Traversal order is not output order in LC 987. You collect (col, row, val) triples during any traversal you like, then sort. Trying to emit in the right order during the walk is the hard way to do it and usually wrong.
Normalisation also keeps the numbers readable while debugging, which matters more than it sounds when you are hand-tracing a deep tree.
Walkthrough — LC 662 width, with per-level normalisation3 steps
A deliberately sparse tree. Watch level three: two nodes, width four.
| # | Level | Raw indices | Normalised | first .. last | Width |
|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 .. 0 | 1 |
| 2 | 2, 3 | 0, 1 | 0, 1 | 0 .. 1 | 2 |
| 3 | 4, 5 | 0, 3 | 0, 3 | 0 .. 3 | 4 |
Answer 4. Level three holds only two nodes, but they sit at normalised indices 0 and 3 — the two absent positions between them are genuinely part of the width. Count nodes instead of subtracting indices and you report 2, which is the single most common wrong answer to this problem.
Key observations — what interviewers are listening for4 points
- Choose the coordinate first, then the traversal. BFS or DFS both work once every node carries an index. Deciding the numbering is the real modelling step.
- Say why normalisation is needed, not just that it is. Indices double per level, so depth 60 overflows
long. Subtracting the level's first index keeps them small permanently. - The third sort key is a genuine interview trap. The gate names it specifically. Two nodes can share a column and a row, and only the value separates them.
- Gaps are data. Width counts the empty positions between the ends is the sentence that makes LC 662 obvious and its absence that makes it confusing.
Common mistakes4 traps
-
Counting nodes per level as the width
Symptom: undercounts every sparse level.
Prevention: Width is
lastIndex - firstIndex + 1under heap indexing. -
Heap indices without per-level normalisation
Symptom: overflow, then negative widths as the numbers wrap.
Prevention: Subtract the level's first index; carry indices as
long. -
Sorting LC 987 by
(col, row)onlySymptom: wrong order among nodes sharing a position.
Prevention: The third sort key is the node's value. No traversal supplies it.
-
Trying to emit vertical order during the walk
Symptom: a complicated traversal that is still in the wrong order.
Prevention: Collect triples, then sort. Traversal order is not output order.
Key takeaway
- Trigger: the answer depends on a node's position — width, column, diagonal.
- Heap indexing:
left = 2i,right = 2i+1; width islast - first + 1. - Two safeguards: normalise per level, and carry indices as
long. - LC 987: collect
(col, row, val)and sort — the value is the third key. - Gate: state the heap-index rule, why normalisation is required, and the third sort key. See §5.1.
F Tree as a graph#
The moment a question asks about distance in any direction, the tree stops being a tree. Add a parent map and it becomes an undirected graph — and top-down DFS cannot answer the question at all.
Mental model
“I can only walk downward, but the answer is k steps away in any direction. So I need to be able to walk up too — and the instant I can, the structure has cycles.”
This is the sub-variant most people never learn, and it is the most transferable one in the pattern. The recognition is a single sentence and it changes the entire approach.
Three steps, and the third is the one people forget: build the parent map, BFS through three neighbours, and carry a visited set.
Parent links make every edge bidirectional, so a node and its parent form a two-node cycle. That is why visited is mandatory rather than an optimisation.
Recognition — reach for this when
- Distance in a tree, measured in any direction — up, down, or across.
- The question is anchored at an arbitrary node rather than at the root.
- Words like k away, nearest, infection spreads, time to reach.
- But not for anything answerable by a downward pass. Building a parent map for a subtree question is pure overhead.
Java863. Three steps, and the third is the one people forget.31 lines
// 863. Three steps, and the third is the one people forget.
// 1. one DFS to record every node's parent
// 2. BFS outward from the target through THREE neighbours: left, right, parent
// 3. a visited set — the graph now contains 2-cycles, so BFS without it never terminates
List<Integer> distanceK(TreeNode root, TreeNode target, int k) {
Map<TreeNode, TreeNode> par = new HashMap<>();
link(root, null, par);
Deque<TreeNode> q = new ArrayDeque<>();
Set<TreeNode> seen = new HashSet<>();
q.add(target); seen.add(target);
for (int d = 0; !q.isEmpty(); d++) {
if (d == k) { // the whole frontier is the answer
List<Integer> out = new ArrayList<>();
for (TreeNode n : q) out.add(n.val);
return out;
}
for (int i = q.size(); i > 0; i--) {
TreeNode n = q.poll();
for (TreeNode nb : new TreeNode[]{n.left, n.right, par.get(n)})
if (nb != null && seen.add(nb)) q.add(nb);
}
}
return List.of();
}
void link(TreeNode n, TreeNode p, Map<TreeNode, TreeNode> par) {
if (n == null) return;
par.put(n, p);
link(n.left, n, par);
link(n.right, n, par);
}Why it works — why downward DFS cannot answer it, and why visited is mandatory4 steps
Two claims. The first tells you to change tools; the second stops the new tool from hanging.
- 1
Downward-only recursion cannot reach the answer. From the target node, a node
ksteps away may be a sibling, an ancestor, or in a completely different branch. A top-down DFS has no route to any of them — it is not a matter of being slow, it is a matter of being unable. - 2
A parent map makes the structure undirected. Record every node's parent once, and each node now has up to three neighbours:
left,right,parent. Distance in the tree becomes ordinary graph distance. - 3
Which means cycles. An undirected edge between a node and its parent is traversable both ways — a 2-cycle. BFS on a cyclic graph without a
visitedset bounces between them forever. - 4
Build the map before you start. Populating it lazily during the BFS misses ancestors above the target, because you only discover parents for nodes you have already reached from above.
Say this before writing code — the gate calls it the most transferable in the pattern: parent map + BFS + visited. The visited set is mandatory, not an optimisation: parent links create 2-cycles, so BFS without it never terminates.
At depth k, the entire frontier is the answer. You do not filter — BFS has already grouped the nodes by distance, so the whole queue at that moment is the result.
seen.add(nb) doubles as the enqueue condition. Java's Set.add returns false if the element was already present, so one call both tests and marks — which is why the loop body stays a single line.
Walkthrough — LC 863 — all nodes distance 2 from node 53 steps
Target is node 5, k = 2. Watch the frontier move upward at step 1 — which is precisely what a downward DFS could never do.
| # | Depth | Frontier | Neighbours explored | Seen |
|---|---|---|---|---|
| 1 | 0 | 5 | left 6, right 2, parent 3 | 5, 6, 2, 3 |
| 2 | 1 | 6, 2, 3 | 6 and 2 are leaves; 3's neighbours are 5 (seen) and 1 | + 1 |
| 3 | 2 | 1 | depth k reached — stop | -- |
Answer [1]. Node 1 is two steps away only by going up from 5 to 3 and then down — there is no downward path from 5 to 1 at all. Notice also step 2: node 3's neighbour list includes 5, which is already seen; without the visited set the search would walk straight back down and oscillate forever.
Key observations — what interviewers are listening for5 points
- The recognition is the skill, not the code. Distance in any direction means parent map plus BFS plus visited. Saying that before writing anything is exactly what the gate tests.
- Name why
visitedis mandatory. Not for efficiency — for termination. Parent links create 2-cycles and the BFS would never stop. - Build the parent map eagerly. One complete DFS first. Lazy population is a subtle bug that only shows up on targets deep in the tree.
- The frontier is the answer. BFS groups by distance for free, so no filtering pass is needed at depth
k. - This generalises past trees. Once you see a tree is a graph with extra promises, problems about infection spread, nearest leaf and time-to-reach all become the same BFS.
Common mistakes4 traps
-
Graph BFS on a tree without
visitedSymptom: infinite loop between a node and its parent.
Prevention: Parent links create 2-cycles. Use
seen.add(nb)as the enqueue condition. -
Building the parent map lazily during the BFS
Symptom: missing ancestors above the target.
Prevention: The parent map must be complete before the BFS starts.
-
Trying to solve it with a downward DFS
Symptom: you can reach descendants and nothing else.
Prevention: The question is undirected. Change the structure, not the traversal.
-
Filtering the frontier at depth
kSymptom: unnecessary work, and a chance to filter wrongly.
Prevention: Everything in the queue at depth
kis exactlykaway. Return the lot.
Key takeaway
- Trigger: distance in a tree, in any direction, anchored anywhere.
- Three steps: complete parent map -> BFS through left, right, parent ->
visitedset. - Why visited: for termination — parent links create 2-cycles.
- Read-off: at depth
kthe whole frontier is the answer. - Gate: say parent map + BFS + visited before coding, and explain why the visited set is mandatory. See §5.1.
G Morris traversal, O(1) space#
Borrow the tree's own null pointers as breadcrumbs. Thread a subtree's rightmost node back to its ancestor so you can climb without a stack — then destroy the thread before you leave.
Mental model
“I have no stack, so the tree itself has to remember where I should return to. The predecessor's right pointer is null and therefore free — I will borrow it, and I will put it back.”
This sub-variant exists for exactly one interview sentence: can you do it in O(1) space? Knowing it is the difference between answering that question and conceding it.
The predecessor is the rightmost node of the left subtree, and pred.right == cur is the marker meaning I have been here before — this is the second visit.
Every thread is destroyed before its node is emitted, so the tree is bit-identical when the loop exits. An interviewer will ask you to prove exactly that.
Recognition — reach for this when
- The problem explicitly demands
O(1)extra space. - You are allowed to mutate the tree temporarily, provided you restore it.
- An inorder walk is what you need — Morris is most natural there.
- But not when the tree must not be touched at all, even transiently. Concurrent readers make threading unsafe.
JavaINVARIANT: every thread created is destroyed before its node is emitted, so the tree is16 lines
// INVARIANT: every thread created is destroyed before its node is emitted, so the tree is
// bit-identical when the loop exits. An interviewer WILL ask you to prove that.
// The predecessor is the rightmost node of the left subtree; "pred.right == cur" is the
// marker that says "I have been here before — this is the second visit".
List<Integer> morrisInorder(TreeNode root) {
List<Integer> out = new ArrayList<>();
TreeNode cur = root;
while (cur != null) {
if (cur.left == null) { out.add(cur.val); cur = cur.right; continue; }
TreeNode pred = cur.left;
while (pred.right != null && pred.right != cur) pred = pred.right;
if (pred.right == null) { pred.right = cur; cur = cur.left; } // 1st visit: thread
else { pred.right = null; out.add(cur.val); cur = cur.right; } // undo
}
return out;
}Why it works — the threading invariant and its proof obligation4 steps
Morris is short and every line is load-bearing. The proof that the tree survives is the part you will actually be asked for.
- 1
No stack means the tree must remember. Recursion and an explicit stack both store the return path outside the tree. With
O(1)space neither is available, so the path has to live in the structure. - 2
The predecessor has a free pointer. The rightmost node of
cur's left subtree is the node visited immediately beforecurin inorder, and itsrightis null by definition of being rightmost. That null is the slot to borrow. - 3
First visit: thread and descend.
pred.right = currecords where to return, then go left. You will come back up through that thread automatically. - 4
Second visit: undo, emit, continue. Arriving with
pred.right == curproves the left subtree is finished. Setpred.right = nullfirst, restoring the tree, then emitcurand move right.
The invariant an interviewer will ask you to prove: every thread created is destroyed before its node is emitted, so the tree is bit-identical when the loop exits.
Total work is still O(n): each edge is walked at most twice, once to build a thread and once to find it again. Say amortized O(n), constant space and be ready to justify the factor of two.
The loop condition needs both terms. pred.right != null alone spins forever on the second visit, because the thread you created is exactly what makes pred.right non-null. The second term pred.right != cur is what recognises your own breadcrumb.
Walkthrough — Morris inorder on a three-node tree4 steps
2 with children 1 and 3. Two visits to the root: one to thread, one to undo and emit.
| # | cur | pred | Test | Action | Emitted |
|---|---|---|---|---|---|
| 1 | 2 | 1 | pred.right == null | thread: 1.right = 2; cur = 1 | -- |
| 2 | 1 | -- | cur.left == null | emit 1; cur = cur.right -> follows the thread back to 2 | 1 |
| 3 | 2 | 1 | pred.right == cur | undo: 1.right = null; emit 2; cur = 3 | 1, 2 |
| 4 | 3 | -- | cur.left == null | emit 3; cur = null, loop ends | 1, 2, 3 |
Inorder 1 2 3, and the tree is back exactly as it started — 1.right was set at step 1 and cleared at step 3, before node 2 was emitted. Step 2 is the pretty part: moving to cur.right from node 1 follows the borrowed thread and climbs back up without any stack at all.
Key observations — what interviewers are listening for4 points
- The undo is the answer, not the threading. Anyone can create a thread. The gate is prove the tree is unmodified at the end, which is entirely about where the undo sits.
- Justify the factor of two, do not hide it. Each edge is walked at most twice — once building a thread, once finding it. Amortized
O(n), constant space is the precise claim. - Both terms of the loop condition earn their place. One detects the end of the subtree, the other detects your own breadcrumb. Dropping either hangs the traversal.
- Know when Morris is inappropriate. It mutates the tree transiently. Under concurrent access, or when mutation is forbidden outright, it is the wrong answer even though it is the clever one.
Common mistakes4 traps
-
Morris without removing the thread
Symptom: the returned tree is corrupt, and a second traversal loops forever.
Prevention: The
elsebranch must setpred.right = nullbefore emitting. -
Loop condition with only
pred.right != nullSymptom: infinite loop on the second visit — your own thread satisfies the condition.
Prevention: Both terms:
pred.right != null && pred.right != cur. -
Emitting before undoing
Symptom: the invariant thread destroyed before emit breaks, and the proof you are asked for is false.
Prevention: Undo, then emit, then move right — in that order.
-
Claiming plain
O(n)without the caveatSymptom: an imprecise complexity claim on a question specifically about cost.
Prevention: Each edge is walked up to twice; say amortized.
Key takeaway
- Trigger: can you do it in
O(1)space? — and mutation is permitted. - The mechanism: thread the left subtree's rightmost node to
cur, descend, then undo on return. - The marker:
pred.right == curmeans second visit. - The invariant: every thread is destroyed before its node is emitted — the tree survives intact.
- Gate: Morris inorder blind, including the undo, and a proof that the tree is unmodified. See §5.1.
H N-ary children#
Swap left/right for a child list and every one of A through D still works. The only genuinely new decision is the identity element for your aggregate.
Mental model
“The shape of the recursion does not change — only how I enumerate children. What does change is what I return when there are none.”
This sub-variant is short because the generalisation is nearly free. Two fields become a loop; everything else about the traversal is untouched.
The one place it bites is the base case. With two children you can write max(left, right) and never think about it; with a list you have to name the value that a zero-child fold returns.
max(depth of no children) = 0, so a leaf returns 0 + 1 = 1. Choose -infinity and every leaf reports nonsense; choose 1 and every depth is one too large.
Recognition — reach for this when
- Nodes carry a list of children rather than two fields.
- You already know the binary form and need the same answer generalised.
- The aggregate is a fold — max, sum, min, count — over the children's results.
- But not when the problem depends on left versus right specifically. Ordering-sensitive logic does not survive the generalisation.
JavaThe identity element for max over ZERO children is 0, not -infinity and not 1.8 lines
// The identity element for max over ZERO children is 0, not -infinity and not 1.
// Get that wrong and every leaf reports the wrong depth.
int maxDepth(Node root) {
if (root == null) return 0;
int best = 0;
for (Node c : root.children) best = Math.max(best, maxDepth(c));
return best + 1;
}Why it works — why the generalisation is free, and where it is not4 steps
Three steps of nothing changes, then the one step that does.
- 1
The child list replaces the two fields.
node.leftandnode.rightwere only ever a fixed-size collection of children. A list is the same thing without the arity restriction. - 2
The loop replaces the two calls.
for (Node c : node.children) dfs(c);is exactly the two recursive calls, generalised. Pre- and postorder still mean before the loop and after the loop. - 3
BFS needs no change at all. Enqueue every child instead of two. The size snapshot from sub-variant C is untouched, so level-order and its derivatives generalise for free.
- 4
The identity element does change. A fold over an empty list must return something, and that value is the base case. It is the only decision the binary form let you avoid making.
The only genuinely new decision: the identity element for max over zero children is 0, not -infinity and not 1. Get that wrong and every leaf reports the wrong depth.
Pick the identity by asking what the fold should return for nothing. Sum of no numbers is 0; max of no depths is 0 because depth is non-negative; min of no values is +infinity. Stating it that way makes it a derivation rather than a memory test.
Ordering-sensitive logic does not generalise. Anything phrased as left before right — the DFS form of LC 199, inorder itself — has no meaning once children are an unordered list.
Walkthrough — n-ary max depth6 steps
Root with three children, one of which has two children of its own. The fold at each node is max over children, then + 1.
| # | Node | Children | max over children | Returns |
|---|---|---|---|---|
| 1 | 2 | none | 0 (the identity) | 0 + 1 = 1 |
| 2 | 5 | none | 0 | 1 |
| 3 | 6 | none | 0 | 1 |
| 4 | 3 | 5, 6 | max(1, 1) = 1 | 2 |
| 5 | 4 | none | 0 | 1 |
| 6 | 1 | 2, 3, 4 | max(1, 2, 1) = 2 | 3 |
Depth 3. Rows 1, 2, 3 and 5 are all the identity doing its job: a leaf's loop never executes, best stays 0, and the node returns 1. Initialise best to -infinity instead and every one of those rows returns garbage that then poisons the fold above it.
Key observations — what interviewers are listening for4 points
- Derive the identity, do not recall it. What should this fold return for no children? Sum: 0. Max of a non-negative quantity: 0. Min:
+infinity. The gate asks you to state it. - The generalisation is genuinely free for A through D. Saying so — and meaning it — is the point of this sub-variant. It is not a new technique, it is the same ones with the arity restriction lifted.
- Watch for logic that assumed two children. Anything that says left or right by name is not generalisable, and that is worth noticing before you start rather than after.
- Null children still need guarding. A child list can contain nulls depending on the problem's representation. The callee guard from sub-variant A still applies.
Common mistakes4 traps
-
Initialising the fold to
-infinityfor a depthSymptom: every leaf returns nonsense, which propagates up the whole tree.
Prevention: The identity for max over zero children is 0. A leaf is depth 1.
-
Initialising to 1
Symptom: every depth comes out one too large.
Prevention: The
+ 1for the node itself happens once, at the return. The identity is the children's contribution, which is 0. -
Generalising order-sensitive logic
Symptom: left-versus-right reasoning has no meaning over a list.
Prevention: Check whether the binary version depended on ordering before porting it.
-
Rewriting BFS for n-ary
Symptom: wasted effort, and a fresh chance to drop the size snapshot.
Prevention: Enqueue all children instead of two. Nothing else about sub-variant C changes.
Key takeaway
- Trigger: a child list instead of
left/right. - The change: two recursive calls become one loop; BFS enqueues all children.
- The one decision: the identity element — what the fold returns for zero children.
- Depth: identity 0, so a leaf returns 1. Not
-infinity, not 1. - Gate: generalise any of A-D without re-deriving, and state the identity element for the aggregate. See §5.1.
1.4 Failure Modes — Traversal#
| # | Bug | Symptom | Prevention |
|---|---|---|---|
| 1 | Guarding the caller (if (n.left != null) dfs(n.left)) | Duplicated null logic, missed base cases as the function grows | Guard the callee: one if (node == null) return; at the top, always. |
| 2 | BFS without the q.size() snapshot | Levels merge into one flat list | Take int sz = q.size(); before the inner loop, never inside its condition. |
| 3 | 1 + min(left, right) for minimum depth | Wrong on every one-child node | A leaf is "both children null". Handle the one-child case explicitly. |
| 4 | Zigzag implemented by reversing the queue | Correct level 1, corrupt everything below | Reverse the output list, never the frontier. |
| 5 | Counting nodes per level as "width" | Undercounts every sparse level | Width is lastIndex - firstIndex + 1 under heap indexing. |
| 6 | Heap indices without per-level normalization | Overflow, then negative widths | Subtract the level's first index; carry indices as long. |
| 7 | Vertical order sorted only by (col, row) | Wrong order among equal positions | The third sort key is the node's value. Nothing gives it to you for free. |
| 8 | Graph BFS on a tree without visited | Infinite loop between a node and its parent | Parent links create 2-cycles. seen.add(nb) as the enqueue condition. |
| 9 | Building the parent map lazily during BFS | Missing ancestors above the target | The parent map must be complete before the BFS starts. |
| 10 | Morris without removing the thread | The returned tree is corrupt; a second traversal loops forever | The else branch must set pred.right = null before emitting. |
| 11 | Morris with while (pred.right != null) only | Infinite loop on the second visit | The loop condition needs both terms: pred.right != null && pred.right != cur. |
| 12 | Reverse-preorder postorder used for side effects | Node processed before its children are done | It produces the right list, not the right timing. Use recursion or the lastVisited form. |
| 13 | Missing null-root guard in a BFS template | NPE on the first q.add(root) dereference | Recursive DFS tolerates a null root; BFS never does. |
| 14 | Deque used as a queue with push | Silent DFS instead of BFS | add/poll for FIFO, push/pop for LIFO. Never mix them in one method. |