Trees, No Gaps — Traversal · Tree Recursion · Binary Search Trees Library
Show

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-variantOrder imposed byState carriedSpace
ARecursive DFS — the three ordersthe call stackthe implicit root-pathO(h)
BIterative DFS — explicit stacka stack you ownwhatever you pushO(h)
CBFS — level ordera FIFO queue + a size snapshotthe current frontierO(w)
DLevel-order derivativesas C, then a per-level ruleone accumulator per levelO(w)
ECoordinate-indexed traversala computed (row, col) or heap indexan index per nodeO(n)
FTree as a grapha parent map + BFS from any nodevisited — trees stop being acyclic-by-directionO(n)
GMorris threadingtemporary right-child threadsnoneO(1)
HN-ary / generalized childrena child list instead of left/rightas A–DO(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#ProblemDiffSub-variantWhy it's essential
1144. Binary Tree Preorder TraversalEasyAThe atom. Establishes the base case if (node == null) return; and the fact that "visit" is one line you can move.
294. Binary Tree Inorder TraversalEasyAMoving the visit line one position changes the entire output order. This is the whole lesson of sub-variant A.
3145. Binary Tree Postorder TraversalEasyAPostorder 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.
4589. N-ary Tree Preorder TraversalEasyA + HSame recursion with a child list. Do it only if the generalization isn't obvious.

B Iterative DFS with an explicit stack#

Solved#ProblemDiffSub-variantWhy it's essential
594. Binary Tree Inorder Traversal (iterative)EasyBThe 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.
6145. Binary Tree Postorder Traversal (iterative)EasyBReverse-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.
7173. Binary Search Tree IteratorMediumBA 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().
8331. Verify Preorder Serialization of a Binary TreeMediumBSlot counting — a traversal with no tree at all. Elegant, occasionally asked, teaches nothing structural.

C BFS: level order with the size snapshot#

Solved#ProblemDiffSub-variantWhy it's essential
9102. Binary Tree Level Order TraversalMediumCThe 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 TreeEasyCThe 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.
11107. Binary Tree Level Order Traversal IIMediumC#9 with addFirst. Two-minute warm-up, nothing new.

D Level-order derivatives#

Solved#ProblemDiffSub-variantWhy it's essential
12199. Binary Tree Right Side ViewMediumD"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.
13103. Binary Tree Zigzag Level Order TraversalMediumDThe direction flag belongs to the output list, not the queue. Reversing the queue is the standard wrong turn.
14637. Average of Levels in Binary TreeEasyDPure rep. Watch the long accumulator.
15515. Find Largest Value in Each Tree RowMediumDPure rep.
161161. Maximum Level Sum of a Binary TreeMediumDRep with a 1-indexed answer; the off-by-one is the only content.

E Coordinate-indexed traversal#

Solved#ProblemDiffSub-variantWhy it's essential
17662. Maximum Width of Binary TreeMediumEHeap 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 TreeHardEThe 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.
19314. Binary Tree Vertical Order Traversal PROMediumEThe easy version of #18 — no value tie-break. Free substitute: solve 987 and drop the third sort key.

F Tree as a graph#

Solved#ProblemDiffSub-variantWhy it's essential
20⚠︎863. All Nodes Distance K in Binary TreeMediumFThe 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.
212385. Amount of Time for Binary Tree to Be InfectedMediumFIdentical 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.
22742. Closest Leaf in a Binary Tree PROMediumFSame parent-map BFS with a leaf predicate. Free substitute: 863.

G Morris traversal#

Solved#ProblemDiffSub-variantWhy it's essential
2394. Binary Tree Inorder Traversal (Morris, O(1) space)EasyGExists 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#ProblemDiffSub-variantWhy it's essential
24429. N-ary Tree Level Order TraversalMediumHBFS where the frontier expansion is a loop, not two lines. Confirms that C generalizes without change.
25559. Maximum Depth of N-ary TreeEasyHAggregation over a child list: the identity element for max over zero children is 0, and getting that wrong is the only bug available.
26590. N-ary Tree Postorder TraversalEasyHRedundant after #24 and #3.

Extra Reps — Traversal (only if a gate fails)#

SolvedProblemTargets
993. Cousins in Binary TreeDepth and parent recorded in one pass.
1302. Deepest Leaves SumLevel accumulator with a reset.
623. Add One Row to TreeLevel-order with structural insertion mid-traversal.
1609. Even Odd TreePer-level monotonicity + parity, four failure conditions in one loop.
671. Second Minimum Node In a Binary TreePruned DFS — stop descending when the invariant says you cannot improve.
366. Find Leaves of Binary Tree PROHeight-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.

void dfs(node): if (node == null) return <- guard the CALLEE, once, at the top // visit here -> PREORDER before both children dfs(node.left) // visit here -> INORDER between the two children dfs(node.right) // visit here -> POSTORDER after both children only in POSTORDER have both recursive calls RETURNED -> only postorder can compute an aggregate over the subtree

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. 1

    The invariant. dfs(node) completely visits the subtree rooted at node and touches nothing outside it. That containment is what lets you reason about one node at a time.

  2. 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. 3

    The base case. null is the empty subtree, a perfectly legal input. So the guard belongs at the top of the callee: one check, one place.

  4. 4

    Why postorder computes aggregates. At the moment a postorder visit runs, both dfs calls 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.

1 / \ 2 3 / \ 4 5
#MomentPreorderInorderPostorder
1enter 11
2enter 22
3enter and leave 4 (a leaf)444
4back at 2, left child done2
5enter and leave 5 (a leaf)555
6leave 2 — both children done2
7back at 1, left subtree done1
8enter and leave 3, then leave 1333, 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 to O(n) on a degenerate tree. Saying O(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 space

    Symptom: stack overflow on a degenerate, list-shaped tree.

    Prevention: Space is O(h). Only a balanced tree makes that O(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.

INORDER, iterative while (cur != null || stack nonempty): descend left, pushing every node pop -> emit -> move to its RIGHT child INVARIANT: the stack holds exactly those ancestors whose LEFT subtree is finished and whose own value has NOT yet been emitted POSTORDER, by reversed preorder push root; pop -> addFirst; push LEFT then RIGHT (so RIGHT pops first) Root-Right-Left emitted front-first == Left-Right-Root CAVEAT: correct LIST, wrong MOMENTS

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. 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. 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. 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 addFirst so the reversal happens without a second pass.

  4. 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.

1 / \ 2 3 / \ 4 5
#curStack (top first)ActionEmitted so far
11 -> 2 -> 44, 2, 1descend left, pushing each--
2--2, 1pop 4, emit, go to its right (null)4
3--1pop 2, emit, go to its right -> 54, 2
455, 1descend from 5 (no left child)4, 2
5--1pop 5, emit, right is null4, 2, 5
6--(empty)pop 1, emit, go right -> 3, push and pop it4, 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 n calls cost O(n) overall. That is the whole answer to why is next() amortized O(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 lastVisited form.

  • Mixing push/pop with add/poll on one Deque

    Symptom: a silent DFS where you intended a BFS, or vice versa.

    Prevention: add/poll for FIFO, push/pop for 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 lastVisited when timing matters.
  • Gate: iterative inorder blind with the invariant stated, then LC 173 and why next() is amortized O(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.

while (!q.isEmpty()): int sz = q.size(); <- SNAPSHOT, before the inner loop for (i = 0; i < sz; i++): <- NOT i < q.size() n = q.poll() ... accumulate into this level ... enqueue n.left, n.right <- the queue GROWS while you drain it sz freezes the level boundary at the moment the level began.

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. 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. 2

    Draining while enqueueing breaks it. Each poll removes a node of the current level and each add appends a node of the next one, so q.size() changes meaning mid-loop.

  3. 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. 4

    The null-root guard. BFS dereferences root immediately 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.

1 / \ 2 3 / \ 4 5
#Queue at level startszDrainedEnqueued duringLevel output
11112, 3[1]
22, 322, 34, 5[2, 3]
34, 524, 5nothing[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), not O(h). The widest level dominates. On a complete tree that is n/2, which is worth saying when comparing against DFS's O(h).
Common mistakes4 traps
  • BFS without the q.size() snapshot

    Symptom: 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 depth

    Symptom: 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: NullPointerException on the first q.add(root) dereference.

    Prevention: Recursive DFS tolerates a null root; BFS never does. Guard explicitly.

  • Using Deque as a queue with push

    Symptom: silent DFS instead of BFS — the output looks plausible and is wrong.

    Prevention: add/poll for FIFO. push/pop is 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.

199, two ways -- know both and which is which: BFS take the LAST node of each level O(w) space DFS visit RIGHT first; record when depth == out.size() O(h) space 103 zigzag -- the direction flag belongs to the OUTPUT list: if (l2r) level.addLast(v); else level.addFirst(v); NEVER reverse the queue -- that also reverses the children's enqueue order and corrupts every level below.

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 or O(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. 1

    The skeleton does not change. Sub-variant C's loop, with the snapshot, is still underneath. Only the accumulator differs.

  2. 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 is O(h) rather than O(w).

  3. 3

    Zigzag's tempting mistake. Reversing the frontier looks like it produces alternating output, and it does — for one level.

  4. 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.

1 / \ 2 3 / \ 4 5
#Level drained (always L to R)DirectionAppendsLevel output
11left to rightaddLast(1)[1]
22, 3right to leftaddFirst(2), then addFirst(3)[3, 2]
34, 5left to rightaddLast(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 versus O(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 available

    Symptom: O(w) space where O(h) would do — up to n/2 versus log n on 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 with depth == out.size() is O(h).
  • Zigzag: flip the output, never the queue.
  • Gate: write 199 both ways and say which is O(h) and which is O(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.

662 WIDTH -- heap indexing: left = 2i, right = 2i + 1 width of a level = lastIndex - firstIndex + 1 -- an index DIFFERENCE, so the MISSING nodes between the ends still count normalise: subtract each level's first index before recursing carry as long: a deep alternating tree blows past int 987 VERTICAL ORDER -- traversal order is NOT output order sort key = (col, row, val) ^^^ the third key is the trap: no traversal produces it for you

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. 1

    Heap indexing. Assign the root index 0, then left = 2i and right = 2i + 1. This is the same numbering an array-backed heap uses, and it encodes horizontal position exactly.

  2. 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. 3

    Normalisation is mandatory. Raw indices double each level, so a deep alternating tree overflows — first int, then even long. Subtract the level's first index as you go, and carry the index as long.

  4. 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.

1 index 0 / \ 2 3 indices 0, 1 (after normalising) / \ 4 5 indices 0, 3 <- two nodes, width 4
#LevelRaw indicesNormalisedfirst .. lastWidth
11000 .. 01
22, 30, 10, 10 .. 12
34, 50, 30, 30 .. 34

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 + 1 under 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) only

    Symptom: 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 is last - 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.

a tree walked downward only: a tree with parent links: (root) (root) | | ^ v v | (node) (node) <- a 2-CYCLE THREE STEPS 1. one DFS to record every node's parent -- BEFORE the BFS starts 2. BFS outward through THREE neighbours: left, right, parent 3. a visited set -- the graph now has 2-cycles, so BFS without it never terminates at depth k, the WHOLE FRONTIER is the answer

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. 1

    Downward-only recursion cannot reach the answer. From the target node, a node k steps 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. 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. 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 visited set bounces between them forever.

  4. 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.

3 / \ 5 1 / \ 6 2 parent map: 5->3, 1->3, 6->5, 2->5 target = 5, k = 2
#DepthFrontierNeighbours exploredSeen
105left 6, right 2, parent 35, 6, 2, 3
216, 2, 36 and 2 are leaves; 3's neighbours are 5 (seen) and 1+ 1
321depth 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 visited is 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 visited

    Symptom: 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 k

    Symptom: unnecessary work, and a chance to filter wrongly.

    Prevention: Everything in the queue at depth k is exactly k away. 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 -> visited set.
  • Why visited: for termination — parent links create 2-cycles.
  • Read-off: at depth k the 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.

FIRST visit to cur (pred.right == null): pred.right = cur <- create the thread, a breadcrumb back up cur = cur.left <- descend SECOND visit (pred.right == cur): pred.right = null <- UNDO the thread, restoring the tree emit cur cur = cur.right <- climb onward loop condition needs BOTH terms: while (pred.right != null && pred.right != cur) ^ without this, the second visit loops forever

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. 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. 2

    The predecessor has a free pointer. The rightmost node of cur's left subtree is the node visited immediately before cur in inorder, and its right is null by definition of being rightmost. That null is the slot to borrow.

  3. 3

    First visit: thread and descend. pred.right = cur records where to return, then go left. You will come back up through that thread automatically.

  4. 4

    Second visit: undo, emit, continue. Arriving with pred.right == cur proves the left subtree is finished. Set pred.right = null first, restoring the tree, then emit cur and 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.

2 / \ 1 3 predecessor of 2 is 1 (rightmost of the left subtree)
#curpredTestActionEmitted
121pred.right == nullthread: 1.right = 2; cur = 1--
21--cur.left == nullemit 1; cur = cur.right -> follows the thread back to 21
321pred.right == curundo: 1.right = null; emit 2; cur = 31, 2
43--cur.left == nullemit 3; cur = null, loop ends1, 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 else branch must set pred.right = null before emitting.

  • Loop condition with only pred.right != null

    Symptom: 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 caveat

    Symptom: 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 == cur means 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.

binary n-ary ------------------------------ ------------------------------ dfs(node.left) for (Node c : node.children) dfs(node.right) dfs(c) IDENTITY ELEMENTS -- the answer for an EMPTY child list: max for depth -> 0 NOT -infinity, and NOT 1 sum -> 0 min -> +infinity count -> 0

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. 1

    The child list replaces the two fields. node.left and node.right were only ever a fixed-size collection of children. A list is the same thing without the arity restriction.

  2. 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. 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. 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.

1 / | \ 2 3 4 / \ 5 6
#NodeChildrenmax over childrenReturns
12none0 (the identity)0 + 1 = 1
25none01
36none01
435, 6max(1, 1) = 12
54none01
612, 3, 4max(1, 2, 1) = 23

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 -infinity for a depth

    Symptom: 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 + 1 for 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#

#BugSymptomPrevention
1Guarding the caller (if (n.left != null) dfs(n.left))Duplicated null logic, missed base cases as the function growsGuard the callee: one if (node == null) return; at the top, always.
2BFS without the q.size() snapshotLevels merge into one flat listTake int sz = q.size(); before the inner loop, never inside its condition.
31 + min(left, right) for minimum depthWrong on every one-child nodeA leaf is "both children null". Handle the one-child case explicitly.
4Zigzag implemented by reversing the queueCorrect level 1, corrupt everything belowReverse the output list, never the frontier.
5Counting nodes per level as "width"Undercounts every sparse levelWidth is lastIndex - firstIndex + 1 under heap indexing.
6Heap indices without per-level normalizationOverflow, then negative widthsSubtract the level's first index; carry indices as long.
7Vertical order sorted only by (col, row)Wrong order among equal positionsThe third sort key is the node's value. Nothing gives it to you for free.
8Graph BFS on a tree without visitedInfinite loop between a node and its parentParent links create 2-cycles. seen.add(nb) as the enqueue condition.
9Building the parent map lazily during BFSMissing ancestors above the targetThe parent map must be complete before the BFS starts.
10Morris without removing the threadThe returned tree is corrupt; a second traversal loops foreverThe else branch must set pred.right = null before emitting.
11Morris with while (pred.right != null) onlyInfinite loop on the second visitThe loop condition needs both terms: pred.right != null && pred.right != cur.
12Reverse-preorder postorder used for side effectsNode processed before its children are doneIt produces the right list, not the right timing. Use recursion or the lastVisited form.
13Missing null-root guard in a BFS templateNPE on the first q.add(root) dereferenceRecursive DFS tolerates a null root; BFS never does.
14Deque used as a queue with pushSilent DFS instead of BFSadd/poll for FIFO, push/pop for LIFO. Never mix them in one method.