🌳 Trees & BST

A learning sheet β€” trees are recursion made visible. Master one question β€” "what does each call return to its parent?" β€” and most tree problems fall in three lines.

🧠 1 Β· How to think β€” the return-value contract

A binary tree node is { val, left, right }. Almost every solution is one recursive function over subtrees. The skill that unlocks all of them is being ruthlessly clear about the contract of your recursion. Before writing a line, answer three questions:

🏒 The mental model β€” an org chart Picture the tree as a company. dfs(node) is a manager: they ask each direct report (child) for one summary number, fold those together, and report one number up to their own boss β€” without ever redoing a report's work. Trusting that summary blindly is the recursion leap of faith (your weak spot β€” lean on it). The three questions below are just that manager's job description.
The 3-question contract (1) RETURN: what single thing does dfs(node) hand back to its parent? (e.g. "the height of this subtree") (2) BASE CASE: what does dfs(null) return? (e.g. 0) (3) COMBINE: how does the parent fold its children's returns into its own answer β€” and does it also record something in a global along the way?

The single biggest tree confusion is mixing up what you return (one value, for the parent to keep recursing) with what you record (a global "best", which may use both children at once). Diameter is the perfect teacher.

Watch it get invented β€” Diameter of a Binary Tree

  1. Warm-up: height. height(node) RETURNS the node-count of the tallest path down. Base: height(null) = 0. Combine: 1 + max(height(left), height(right)).
  2. The twist. The diameter (longest path between any two nodes) bends at some node and uses both sides: leftHeight + rightHeight. But the parent can't use a "bent" length β€” it needs a straight height. So dfs must RETURN the height while RECORDING left + right into a global.
  3. Payoff. One O(n) pass; the global holds the answer even though the function returns something else.
1 2 3 4 5 ↑ returns h=3 ↑ h=2↑ h=1 h=1h=1 at node 1: record L(2) + R(1) = 3 diameter = 3
Each call returns a height (green, upward); the global diameter is recorded (violet) from both children at the bending node.
Return vs record β€” the cheat table Burn this in; it covers most "DFS that computes a value" problems.
Problemdfs RETURNS (for the parent)RECORDS in a global
Height / Max Depth1 + max(L, R)β€” (the return is the answer)
Diameter1 + max(L, R) (height)max(best, L + R)
Balanced?height, or -1 if a subtree is unbalancedβ€” (propagate -1 upward)
Max Path Sumval + max(L, R, 0) (straight down)max(best, val + max(L,0) + max(R,0))

🎯 2 · Attack plan

AskIf yes β†’Pattern
Just visit nodes in an order (level-by-level, in/pre/post)?DFS recursion or a queue (BFS)Traversals (Β§4.1)
Compute a value from subtrees (height, diameter, sum, path)?the return-value contractReturn-value DFS (Β§4.2)
It's a BST (left < node < right)?exploit the ordering β€” inorder is sortedBST = ordered (Β§4.3)
Lowest common ancestor / a path between nodes?postorder "bubble-up"LCA & paths (Β§4.4)
Build a tree from traversals, or serialize it?preorder gives the root; recurseConstruct (Β§4.5)
Anti-patterns Β· Deep/skewed tree β†’ recursion can stack-overflow; switch to an explicit stack, BFS, or Morris traversal (O(1) space).
Β· DFS vs BFS: BFS for "level", "shortest", "nearest"; DFS for "path", "subtree", "does a path exist". Picking the wrong one makes easy problems hard.
Β· Validating a BST with a local check (left.val < node < right.val) is wrong β€” you must bound by ancestor ranges.

πŸ”Ž 3 Β· Recognition map

Words in the promptReach for
"level order", "zigzag", "right side view", "in/pre/post-order"Traversals Β§4.1
"height/depth", "diameter", "balanced", "max path sum", "count/sum where…"Return-value DFS Β§4.2
"BST", "k-th smallest", "validate BST", "search/insert/delete", "range sum BST"BST Β§4.3
"lowest common ancestor", "path sum", "root-to-leaf", "count paths summing to k", "distance between nodes"LCA & paths Β§4.4
"construct from preorder & inorder", "serialize/deserialize", "flatten", "invert"Construct Β§4.5

🧩 4 · The six core patterns

Node shape: class TreeNode { val: number; left: TreeNode|null; right: TreeNode|null }. Each: a one-liner, a πŸ”‘ invariant, a template, a 🎀 interview lens.

4.1 Β· Traversals (DFS & BFS)

β€œPre = me-first, In = left-me-right (sorted for a BST), Post = children-first; BFS = a queue.”

1 2 3 pre 1 2 3  (node, L, R) in 2 1 3  (L, node, R) β€” sorted in a BST post 2 3 1  (L, R, node)
Where you "visit" the node relative to the recursive calls names the order. Postorder = compute children before yourself (the basis of Β§4.2).
πŸ”‘ Invariant β€” why it's safe The invariant: at any instant the DFS call stack holds exactly the path from the root down to the node you're on, and the BFS queue holds exactly one full level. Why that gives the order: a recursive call fully finishes a subtree before it returns, so where you place the visit β€” before, between, or after the two child calls β€” is the only thing that decides pre / in / post. And because the queue never mixes two levels, draining it front-to-back walks the tree strictly top-to-bottom, left-to-right.

Start with the simplest thing that works β€” recursion. All three depth-first orders are the same code; only where you place the visit relative to the two calls changes β€” see it at a glance:

PREORDER Β· node, L, R
if (!n) return;
push(n.val);
go(n.left);
go(n.right);
INORDER Β· L, node, R
if (!n) return;
go(n.left);
push(n.val);
go(n.right);
POSTORDER Β· L, R, node
if (!n) return;
go(n.left);
go(n.right);
push(n.val);

Same skeleton β€” only the highlighted visit line moves.

Full paste-ready recursive pre / in / post functions
// Recursive DFS β€” identical shape; only the line that "visits" moves
function preorder(node: TreeNode | null, out: number[] = []): number[] {
  if (!node) return out;
  out.push(node.val);                 // visit FIRST   β†’ PREORDER  (node, L, R)
  preorder(node.left, out);
  preorder(node.right, out);
  return out;
}
function inorder(node: TreeNode | null, out: number[] = []): number[] {
  if (!node) return out;
  inorder(node.left, out);
  out.push(node.val);                 // visit BETWEEN β†’ INORDER   (L, node, R) β€” sorted in a BST
  inorder(node.right, out);
  return out;
}
function postorder(node: TreeNode | null, out: number[] = []): number[] {
  if (!node) return out;
  postorder(node.left, out);
  postorder(node.right, out);
  out.push(node.val);                 // visit LAST    β†’ POSTORDER (L, R, node) β€” the basis of Β§4.2
  return out;
}

Now all three iteratively β€” an explicit stack replaces the call stack, plus BFS with a queue. Postorder comes in two forms; the note after the code says when to reach for each:

// Iterative INORDER β€” push the left spine, pop, go right
function inorderIter(root: TreeNode | null): number[] {
  const out: number[] = [], st: TreeNode[] = []; let cur = root;
  while (cur || st.length) {
    while (cur) { st.push(cur); cur = cur.left; }   // dive left
    cur = st.pop()!; out.push(cur.val);             // visit
    cur = cur.right;                                // then right
  }
  return out;
}

// Iterative PREORDER β€” stack; push right BEFORE left so left pops first
function preorderIter(root: TreeNode | null): number[] {
  if (!root) return [];
  const out: number[] = [], st: TreeNode[] = [root];
  while (st.length) {
    const n = st.pop()!; out.push(n.val);           // visit on pop
    if (n.right) st.push(n.right);
    if (n.left)  st.push(n.left);
  }
  return out;
}

// Iterative POSTORDER (values only) β€” a preorder variant (node, right, left), then reversed
function postorderIter(root: TreeNode | null): number[] {
  if (!root) return [];
  const out: number[] = [], st: TreeNode[] = [root];
  while (st.length) {
    const n = st.pop()!; out.unshift(n.val);        // push to the FRONT = reverse at the end
    if (n.left)  st.push(n.left);
    if (n.right) st.push(n.right);
  }
  return out;
}

// Iterative POSTORDER (genuine) β€” mark a node "visited" on the stack; process it on the 2nd pop
function postorderIterVisited(root: TreeNode | null): number[] {
  if (!root) return [];
  const out: number[] = [];
  const st: [TreeNode, boolean][] = [[root, false]];   // each entry: [node, seenBefore?]
  while (st.length) {
    const [node, visited] = st.pop()!;
    if (visited) {
      out.push(node.val);                // 2nd pop β†’ children already done β†’ visit now
    } else {
      st.push([node, true]);             // re-push marked, to return AFTER the children
      if (node.right) st.push([node.right, false]);   // push right then left,
      if (node.left)  st.push([node.left, false]);    // so left is popped (and finished) first
    }
  }
  return out;
}

// BFS level order β€” a queue, one level per outer iteration
function levelOrder(root: TreeNode | null): number[][] {
  if (!root) return [];
  const out: number[][] = [];
  let q: TreeNode[] = [root];
  while (q.length) {
    const level: number[] = [], next: TreeNode[] = [];
    for (const x of q) {
      level.push(x.val);
      if (x.left) next.push(x.left);
      if (x.right) next.push(x.right);
    }
    out.push(level);
    q = next;
  }
  return out;
}
Two iterative postorders β€” which to use postorderIter (reverse a modified preorder) is the quickest to write, but it only gives you the values β€” it never actually stops on a node with both children done. When a problem needs you to do real work at each node in true postorder (children guaranteed finished first β€” evaluating an expression tree, freeing/merging subtrees, computing a value bottom-up), use postorderIterVisited: each node is pushed with a visited flag and only processed on its second pop, after its children. It's the most intuitive genuine postorder β€” and a bonus: the same flag template also does preorder and inorder, just by changing where you push the marked node relative to its two children.
πŸ”­ Level-order variants β€” Right Side View (199) Β· Zigzag (103) Β· Vertical Order (314)
All three are levelOrder with one tweak: right-side view keeps only the last value per level; zigzag flips a direction flag and reverses alternate levels; vertical order carries a column index with each node and groups by it (BFS makes ties come out top-down, left-to-right for free β€” 314 is a Meta favorite).
// Right Side View (199) β€” level order, keep only the LAST value of each level
function rightSideView(root: TreeNode | null): number[] {
  if (!root) return [];
  const out: number[] = [];
  const q: TreeNode[] = [root];
  let front = 0;
  while (front < q.length) {
    const levelEnd = q.length;           // this level's boundary
    while (front < levelEnd) {
      const node = q[front++];
      if (front === levelEnd) out.push(node.val);   // last node of the level
      if (node.left) q.push(node.left);
      if (node.right) q.push(node.right);
    }
  }
  return out;
}

// Zigzag (103) β€” level order + a direction flag; reverse alternate levels
function zigzagLevelOrder(root: TreeNode | null): number[][] {
  if (!root) return [];
  const out: number[][] = [];
  const q: TreeNode[] = [root];
  let front = 0;
  let leftToRight = true;
  while (front < q.length) {
    const level: number[] = [];
    const levelEnd = q.length;
    while (front < levelEnd) {
      const node = q[front++];
      level.push(node.val);
      if (node.left) q.push(node.left);
      if (node.right) q.push(node.right);
    }
    if (!leftToRight) level.reverse();
    out.push(level);
    leftToRight = !leftToRight;
  }
  return out;
}

// Vertical Order (314) β€” BFS carrying a column index; group values by column
function verticalOrder(root: TreeNode | null): number[][] {
  if (!root) return [];
  const cols = new Map<number, number[]>();   // column → values (BFS order = top-down, ties L→R)
  const q: [TreeNode, number][] = [[root, 0]];
  let front = 0;
  let minCol = 0, maxCol = 0;
  while (front < q.length) {
    const [node, col] = q[front++];
    if (!cols.has(col)) cols.set(col, []);
    cols.get(col)!.push(node.val);
    minCol = Math.min(minCol, col);
    maxCol = Math.max(maxCol, col);
    if (node.left) q.push([node.left, col - 1]);
    if (node.right) q.push([node.right, col + 1]);
  }
  const out: number[][] = [];
  for (let c = minCol; c <= maxCol; c++) out.push(cols.get(c)!);
  return out;
}
🧡 Morris traversal β€” O(1) space Β· advanced (tap to expand)
Both the call stack and an explicit stack cost O(h) memory. Morris does inorder in O(1) extra space with one trick: before descending into a left subtree, it ties a string back to itself β€” it points the rightmost node of that left subtree (the node you'll visit just before this one, your inorder predecessor) back at the current node. Later, when the left subtree is done, that string is how you climb back up without a stack; you untie it and visit. The only thing to remember: "thread each node to its inorder predecessor so you can return without a stack." That one sentence is the takeaway; the code below makes it concrete β€” it's finicky, so know the idea cold and reproduce the code only if asked for strict O(1) space.
// Morris inorder β€” O(1) space: thread each node to its inorder predecessor, then unthread
function morrisInorder(root: TreeNode | null): number[] {
  const out: number[] = [];
  let cur = root;
  while (cur) {
    if (!cur.left) {                                  // no left subtree β†’ visit, then go right
      out.push(cur.val);
      cur = cur.right;
    } else {
      let pred = cur.left;                            // predecessor = rightmost node of the left subtree
      while (pred.right && pred.right !== cur) pred = pred.right;
      if (!pred.right) {                              // first visit β†’ thread the breadcrumb back to cur
        pred.right = cur;
        cur = cur.left;
      } else {                                        // thread already set β†’ left subtree done; unwire + visit
        pred.right = null;
        out.push(cur.val);
        cur = cur.right;
      }
    }
  }
  return out;
}
🎀 Interview lens What they're really testing: can you write a traversal both recursively and iteratively (with an explicit stack), pick BFS vs DFS for the task, and recall that an inorder walk of a BST comes out sorted. Nail it and they escalate: level order β†’ zigzag β†’ right-side view β†’ vertical-order traversal.

4.2 Β· Return-value DFS (the heart)

β€œCompute children first, fold their returns into yours β€” and record a global if the answer bends.”

Setup: this is the org-chart manager made concrete. State the contract, then the body is literally three lines: (1) BASE β€” what does an empty subtree (null) report? (2) COMBINE β€” ask both children, fold their returns L and R into your answer. (3) RETURN / RECORD β€” hand one summary up, and if the answer can bend here, also write it to a global. Below, the same dfs three ways, each one twist harder: diameter (return-vs-record), balanced (height hijacked by a -1 "failed" sentinel), and the hardest, Max Path Sum.

πŸ”‘ Invariant β€” why it's safe The invariant (say it before you code): every time dfs(node) returns, two things are already true β€” (1) it handed its parent a correct summary of its whole subtree, and (2) it has compared the best answer bending at this node against the global. Why that's enough: (1) lets a parent trust a child's number without re-checking it β€” the org-chart manager trusts each report. (2) means once the root returns, every possible bending point has been weighed, so the global is the true answer even though the function never returns it.
// Diameter β€” RETURN height, RECORD the bending path
function diameterOfBinaryTree(root: TreeNode | null): number {
  let best = 0;
  function height(node: TreeNode | null): number {   // CONTRACT: height of this subtree
    if (!node) return 0;                              // base case
    const L = height(node.left);
    const R = height(node.right);
    best = Math.max(best, L + R);                     // RECORD: path bending at node
    return 1 + Math.max(L, R);                        // RETURN: straight height for parent
  }
  height(root);
  return best;
}

// Balanced? β€” RETURN height, but hijack it with -1 as a "subtree already failed" sentinel
function isBalanced(root: TreeNode | null): boolean {
  function height(node: TreeNode | null): number {   // CONTRACT: height, or -1 if unbalanced below
    if (!node) return 0;
    const L = height(node.left);
    if (L === -1) return -1;              // a subtree already failed β†’ propagate -1 up
    const R = height(node.right);
    if (R === -1) return -1;
    if (Math.abs(L - R) > 1) return -1;   // imbalance detected here β†’ fail
    return 1 + Math.max(L, R);
  }
  return height(root) !== -1;
}

// Max Path Sum β€” clamp negative branches; RETURN a straight path, RECORD a bent one
function maxPathSum(root: TreeNode | null): number {
  let best = -Infinity;
  function gain(node: TreeNode | null): number {      // CONTRACT: best downward path sum from node
    if (!node) return 0;
    const L = Math.max(0, gain(node.left));           // ignore a negative branch
    const R = Math.max(0, gain(node.right));
    best = Math.max(best, node.val + L + R);          // RECORD: path that bends here
    return node.val + Math.max(L, R);                 // RETURN: straight path down
  }
  gain(root);
  return best;
}
πŸ”’ Count Complete Tree Nodes (222) β€” the O(logΒ² n) trick Β· a Google classic
Counting nodes is trivially O(n) β€” but for a complete tree you can do O(logΒ² n). The insight: walk the left edge and the right edge of a subtree; if the two depths are equal, that subtree is perfect and has exactly 2^h βˆ’ 1 nodes β€” no recursion needed. If they differ, recurse into both children β€” but in a complete tree one of them is always perfect, so only one branch keeps going: log n levels Γ— log n edge-walk each.
// Count Complete Tree Nodes (222) β€” spot perfect subtrees by their edge heights
function countNodes(root: TreeNode | null): number {
  if (!root) return 0;
  let lh = 1;
  let l = root.left;
  while (l) { lh++; l = l.left; }        // depth hugging the LEFT edge
  let rh = 1;
  let r = root.right;
  while (r) { rh++; r = r.right; }       // depth hugging the RIGHT edge
  if (lh === rh) return (1 << lh) - 1;   // equal β†’ perfect tree β†’ 2^h βˆ’ 1, done in O(1)
  const L = countNodes(root.left);       // not perfect β†’ recurse; one side always IS perfect,
  const R = countNodes(root.right);      // so only one real branch survives per level
  return 1 + L + R;
}
🎀 Interview lens What they're really testing: can you separate what you return from what you record, name your base case, and justify clamping a negative child at 0 (a branch that only hurts you, you simply don't take). Nail it and they escalate: height β†’ diameter β†’ balanced (-1 sentinel) β†’ max path sum β€” each is the same dfs with one new twist.
🧩 Three more return-value shapes β€” Delete Nodes (1110) Β· Distribute Coins (979) Β· Cameras (968) Β· (tap to expand)
Same contract, new meaning for the return. Delete Nodes returns the node, or null β€” the parent overwrites its child pointer with it. Distribute Coins returns a balance (surplus up / deficit down) and records the moves globally. Cameras returns a state (0 = uncovered Β· 1 = camera Β· 2 = covered). If you can say "what does the parent need from me?", the body writes itself.
// 1110 Delete Nodes and Return Forest β€” return node-or-null, parent re-links
function delNodes(root: TreeNode | null, toDelete: number[]): TreeNode[] {
  const del = new Set(toDelete);
  const forest: TreeNode[] = [];
  function dfs(node: TreeNode | null, isRoot: boolean): TreeNode | null {
    if (!node) return null;
    const gone = del.has(node.val);
    if (isRoot && !gone) forest.push(node);   // my parent was deleted (or I'm the root) β†’ I start a new tree
    node.left = dfs(node.left, gone);         // the RETURN re-links (or cuts) the child
    node.right = dfs(node.right, gone);
    return gone ? null : node;
  }
  dfs(root, true);
  return forest;
}

// 979 Distribute Coins β€” return the net balance, record moves on each edge
function distributeCoins(root: TreeNode | null): number {
  let moves = 0;
  function dfs(n: TreeNode | null): number {  // + = surplus to send up, - = coins needed from above
    if (!n) return 0;
    const l = dfs(n.left), r = dfs(n.right);
    moves += Math.abs(l) + Math.abs(r);       // every unit crossing an edge costs exactly one move
    return n.val + l + r - 1;                 // keep one coin for myself
  }
  dfs(root);
  return moves;
}

// 968 Binary Tree Cameras β€” greedy post-order, 3 states
function minCameraCover(root: TreeNode | null): number {
  let cams = 0;
  function dfs(n: TreeNode | null): 0 | 1 | 2 {   // 0 = not covered Β· 1 = has camera Β· 2 = covered, no camera
    if (!n) return 2;                             // null needs nothing β†’ counts as covered
    const l = dfs(n.left), r = dfs(n.right);
    if (l === 0 || r === 0) { cams++; return 1; }  // a child is uncovered β†’ I MUST hold a camera
    if (l === 1 || r === 1) return 2;              // a child's camera covers me
    return 0;                                      // nothing nearby β†’ ask my parent
  }
  if (dfs(root) === 0) cams++;                     // root ended uncovered β†’ one more camera
  return cams;
}
⚠️ Traps 1110: forgetting node.left = dfs(...) leaves dangling pointers to deleted nodes. 979: use abs β€” a deficit costs as much as a surplus. 968: null β†’ 2, never 0, and never put a camera on a leaf (its parent does it).

4.3 Β· BST = ordered

β€œLeft < node < right β€” so search is binary, and inorder is sorted.”

Setup: a plain tree can force you to check every node β€” O(n). A BST is binary search frozen into a data structure: because left < node < right, one comparison tells you which half to keep and which to throw away β€” exactly like guessing a number in 1–100 with "higher / lower" hints. So you walk a single root-to-leaf path instead of the whole tree: O(h), and h β‰ˆ log n when balanced. Two facts do everything: you navigate by comparison (that's the binary search), and an inorder walk emits values in sorted order.

πŸ”‘ Invariant β€” the range, not a local check The invariant: a node is valid only if its value lies strictly inside the open range (lo, hi) handed down by its ancestors β€” left child tightens hi to the parent's value, right child tightens lo. A purely local "left < node < right" check passes invalid trees (a deep-left node can still violate a far ancestor).

Navigate β€” O(h), one comparison per step (this is the binary search).

// Search (700) β€” walk by comparison
function searchBST(root: TreeNode | null, val: number): TreeNode | null {
  while (root) {
    if (root.val === val) return root;
    if (val < root.val) root = root.left;    // smaller β†’ go left
    else root = root.right;                  // bigger β†’ go right
  }
  return null;
}

// LCA in a BST (235) β€” the split point where the two values diverge
function lcaBST(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
  let node = root;
  while (node) {
    if (p.val < node.val && q.val < node.val) node = node.left;        // both smaller β†’ left
    else if (p.val > node.val && q.val > node.val) node = node.right;  // both bigger β†’ right
    else return node;                                                  // they split here β†’ LCA
  }
  return null;
}

// Inorder successor (285) β€” smallest node greater than p
function inorderSuccessor(root: TreeNode | null, p: TreeNode): TreeNode | null {
  let succ: TreeNode | null = null;
  let cur = root;
  while (cur) {
    if (p.val < cur.val) {
      succ = cur;          // candidate; a smaller one may be further left
      cur = cur.left;
    } else {
      cur = cur.right;     // p.val >= cur.val β†’ successor is further right
    }
  }
  return succ;
}

Validate & k-th β€” both lean on "an inorder walk of a BST is sorted."

// Validate (98) β€” recursive: carry the allowed (lo, hi) range down
function isValidBST(node: TreeNode | null, lo = -Infinity, hi = Infinity): boolean {
  if (!node) return true;
  if (node.val <= lo || node.val >= hi) return false;
  const L = isValidBST(node.left, lo, node.val);
  const R = isValidBST(node.right, node.val, hi);
  return L && R;
}

// Validate (98) β€” iterative: an inorder walk must be strictly increasing
function isValidBSTIter(root: TreeNode | null): boolean {
  const st: TreeNode[] = []; let cur = root, prev = -Infinity;
  while (cur || st.length) {
    while (cur) { st.push(cur); cur = cur.left; }
    cur = st.pop()!;
    if (cur.val <= prev) return false;   // not strictly increasing β†’ invalid
    prev = cur.val;
    cur = cur.right;
  }
  return true;
}

// k-th smallest (230) β€” inorder yields sorted order; stop at the k-th
function kthSmallest(root: TreeNode | null, k: number): number {
  const st: TreeNode[] = []; let cur = root;
  while (cur || st.length) {
    while (cur) { st.push(cur); cur = cur.left; }
    cur = st.pop()!;
    if (--k === 0) return cur.val;
    cur = cur.right;
  }
  return -1;
}

Iterate on demand β€” kthSmallest generalized into an API. Same stack machinery, paused between calls; the classic "what if it's called repeatedly?" follow-up.

// BST Iterator (173) β€” controlled inorder walk; next() is amortized O(1), space O(h)
class BSTIterator {
  private st: TreeNode[] = [];
  constructor(root: TreeNode | null) {
    this.pushLeft(root);                 // stage the smallest: the left spine
  }
  private pushLeft(node: TreeNode | null): void {
    while (node) {
      this.st.push(node);
      node = node.left;
    }
  }
  next(): number {
    const node = this.st.pop()!;         // smallest unseen value
    this.pushLeft(node.right);           // stage its right subtree's left spine
    return node.val;
  }
  hasNext(): boolean {
    return this.st.length > 0;
  }
}

Insert β€” recursive and iterative both just walk to the empty slot and attach.

// Insert (701) β€” recursive: hang the new node where the walk falls off
function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null {
  if (!root) return new TreeNode(val);
  if (val < root.val) root.left = insertIntoBST(root.left, val);
  else root.right = insertIntoBST(root.right, val);
  return root;
}

// Insert (701) β€” iterative: remember the parent, then attach
function insertIntoBSTIter(root: TreeNode | null, val: number): TreeNode | null {
  const node = new TreeNode(val);
  if (!root) return node;
  let cur: TreeNode | null = root, parent: TreeNode = root;
  while (cur) {
    parent = cur;
    if (val < cur.val) cur = cur.left;
    else cur = cur.right;
  }
  if (val < parent.val) parent.left = node;
  else parent.right = node;
  return root;
}
πŸ—‘οΈ Delete a node (450) β€” the 3-case classic Β· advanced (tap to expand)
Deletion is the one tricky BST op. Find the node, then handle three cases: no child β†’ drop it; one child β†’ splice it out; two children β†’ overwrite this node's value with its inorder successor (the smallest value in the right subtree), then delete that successor β€” which now has at most one child, so it falls into an easy case.
// Delete (450) β€” 3 cases; two children β†’ replace with inorder successor
function deleteNode(root: TreeNode | null, key: number): TreeNode | null {
  if (!root) return null;
  if (key < root.val) {
    root.left = deleteNode(root.left, key);
  } else if (key > root.val) {
    root.right = deleteNode(root.right, key);
  } else {                                 // found the node to delete
    if (!root.left) return root.right;     // 0 or 1 child β†’ splice past it
    if (!root.right) return root.left;
    let succ = root.right;                 // 2 children β†’ inorder successor
    while (succ.left) succ = succ.left;    // (smallest in the right subtree)
    root.val = succ.val;                   // copy its value up
    root.right = deleteNode(root.right, succ.val);   // then delete the successor
  }
  return root;
}
🎀 Interview lens What they're really testing: do you bound each node by the range its ancestors allow (not the local "left < node < right" trap), know inorder comes out sorted, and can do the core ops in O(h) β€” very often followed by "now do it iteratively." Nail it and they escalate: validate β†’ k-th smallest β†’ BST iterator (173) β†’ LCA-in-BST β†’ delete β†’ build a balanced BST from a sorted array (108) β†’ name-drop self-balancing (AVL / red-black).

4.4 Β· LCA & paths β€” postorder bubble-up

β€œIf p and q surface from different children, you're standing on their ancestor.”

Setup: "LCA" is literally lowest common ancestor β€” picture two people tracing their family trees upward; the first ancestor they share is the answer. The code finds that merge point by asking each subtree "is p or q below you?": the node that hears "yes" from both sides is where their two paths first meet. Everything bubbles up from the leaves (postorder).

A p q A: left hit βœ“ & right hit βœ“ β†’ LCA left returns p right returns q
Each call returns "the target I found below me" (or null). The first node to receive a hit from both sides is the LCA.
πŸ”‘ Invariant β€” why it's safe The invariant: lca(node) returns a target node if its subtree contains one, else null. A node gets a non-null from both children only when p and q are split across them β€” which makes it their lowest common ancestor. If both are on one side, that side's answer bubbles straight up.
// LCA in a general binary tree
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
  if (!root || root === p || root === q) return root;   // found one (or hit bottom)
  const L = lowestCommonAncestor(root.left, p, q);
  const R = lowestCommonAncestor(root.right, p, q);
  if (L && R) return root;        // p and q on different sides β†’ this is the LCA
  return L ?? R;                  // both on one side (or neither) β†’ pass it up
}
βž• Path Sum III (437) β€” prefix sums on a path Β· the one pattern the other five don't cover
"Count paths summing to k, starting anywhere, going only downward." Brute force re-walks from every node β€” O(nΒ²). The leap is the array trick "two prefix sums that differ by k bracket a subarray summing to k" β€” applied to the current root-to-node path: keep a map of the running sums of all ancestors; at each node, every ancestor whose running sum equals running βˆ’ k is a valid start point. One rule keeps it correct: backtrack the map when you leave a node, so cousins never see sums from a path they're not on.
// Path Sum III (437) β€” count downward paths (any start node) summing to target
function pathSumIII(root: TreeNode | null, targetSum: number): number {
  const prefix = new Map<number, number>();    // running-sum β†’ how many ancestors have it
  prefix.set(0, 1);                            // empty prefix = "path starts at the root"
  let count = 0;
  function dfs(node: TreeNode | null, running: number): void {
    if (!node) return;
    running += node.val;
    count += prefix.get(running - targetSum) ?? 0;         // each match = a valid start point
    prefix.set(running, (prefix.get(running) ?? 0) + 1);   // add myself for my descendants
    dfs(node.left, running);
    dfs(node.right, running);
    prefix.set(running, prefix.get(running)! - 1);         // BACKTRACK β€” leave the path clean
  }
  dfs(root, 0);
  return count;
}
🎀 Interview lens What they're really testing: can you explain the L && R β†’ root bubble-up (a hit from both sides β‡’ you're the merge point), and do you notice the hidden assumption that both nodes actually exist in the tree. Nail it and they escalate: LCA β†’ path sum (root-to-leaf target) β†’ all root-to-leaf paths (backtracking) β†’ distance between two nodes (LCA + depth math).

4.5 Β· Construct & serialize

β€œPreorder hands you the root; inorder tells you where left ends and right begins.”

Setup: building a tree is recursion in reverse β€” identify the root, split the remaining nodes into left/right groups, recurse. Serialization is just a preorder walk with explicit null markers.

πŸ”‘ Invariant β€” why it's safe The invariant: in preorder the first element of any segment is that subtree's root; locating it in inorder splits the rest into exactly the left and right subtrees (everything before it vs after). Each value is placed once, so the reconstruction is unique and O(n).
// Build a tree from preorder + inorder
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
  const pos = new Map(inorder.map((v, i) => [v, i]));
  let p = 0;
  function build(lo: number, hi: number): TreeNode | null {
    if (lo > hi) return null;
    const node = new TreeNode(preorder[p++]);   // preorder front = root
    const mid = pos.get(node.val)!;             // its index in inorder splits L | R
    node.left = build(lo, mid - 1);
    node.right = build(mid + 1, hi);
    return node;
  }
  return build(0, inorder.length - 1);
}

// Serialize / deserialize (preorder with '#' for null)
function serialize(root: TreeNode | null): string {
  const out: string[] = [];
  (function dfs(n: TreeNode | null) {
    if (!n) { out.push('#'); return; }
    out.push(String(n.val));
    dfs(n.left);
    dfs(n.right);
  })(root);
  return out.join(',');
}
function deserialize(data: string): TreeNode | null {
  const vals = data.split(','); let i = 0;
  function build(): TreeNode | null {
    const v = vals[i++];
    if (v === '#') return null;
    const node = new TreeNode(+v);
    node.left = build();
    node.right = build();
    return node;
  }
  return build();
}
πŸ” Same thing with BFS (level order) β€” iterative, and the string mirrors LeetCode's array Β· (tap to expand)
The preorder version above is the shortest, and it mirrors buildTree. But two things make a BFS version worth knowing. (1) The output string is LeetCode's own tree format: 1,2,3,#,#,#,# is exactly [1,2,3,null,null,null,null] β€” a free mnemonic every time you read a tree problem. (2) It's iterative, so it can't stack-overflow on a deep skewed tree (Β§6). The trick both directions share: enqueue both children β€” nulls included β€” so the queue order lines up token-for-token with the string.
// Serialize β€” BFS: drain a queue, emit '#' for null, enqueue BOTH children (nulls too)
function serializeBFS(root: TreeNode | null): string {
  if (!root) return '';
  const q: (TreeNode | null)[] = [root];
  const out: string[] = [];
  let front = 0;
  while (front < q.length) {
    const node = q[front++];          // plain queue drain β€” no per-level bookkeeping needed
    if (!node) { out.push('#'); continue; }
    out.push(String(node.val));
    q.push(node.left);                // enqueue both children, null or not,
    q.push(node.right);               // so their tokens map 1:1 to the queue order
  }
  return out.join(',');
}

// Deserialize β€” BFS: each dequeued node claims the next two tokens as its children
function deserializeBFS(data: string): TreeNode | null {
  if (!data) return null;
  const vals = data.split(',');
  const root = new TreeNode(parseInt(vals[0]));
  const q: TreeNode[] = [root];
  let front = 0, i = 1;
  while (front < q.length && i < vals.length) {
    const curr = q[front++];          // parent waiting to be wired to its two children
    if (vals[i] !== '#') {            // left child
      const left = new TreeNode(parseInt(vals[i]));
      curr.left = left;
      q.push(left);
    }
    i++;
    if (vals[i] !== '#') {            // right child
      const right = new TreeNode(parseInt(vals[i]));
      curr.right = right;
      q.push(right);
    }
    i++;
  }
  return root;
}
Note Emitting out.join(',') (rather than appending val + ',') avoids a trailing comma, so i < vals.length is the clean bound. If you do build the string with a trailing comma, split leaves a stray '' token at the end β€” then you need i < vals.length - 1, or that empty string becomes TreeNode(NaN).
🎀 Interview lens What they're really testing: can you explain why preorder's front is always the root and inorder's split gives left vs right, and why serialization needs explicit null markers (without them the string is ambiguous). Nail it and they escalate: pre+in β†’ in+post β†’ serialize/deserialize (DFS or BFS) β†’ "Flatten Binary Tree to a Linked List."
🌲 Generate ALL BSTs (95) β€” recursion that returns a list Β· (tap to expand)
Contract: build(lo, hi) returns every BST over the values lo..hi. Pick each root r, then take the cross product of every left tree from build(lo, r-1) with every right tree from build(r+1, hi). Pairs with 96 (count them) β€” 95 is the enumeration.
function generateTrees(n: number): (TreeNode | null)[] {
  function build(lo: number, hi: number): (TreeNode | null)[] {
    if (lo > hi) return [null];                 // ONE empty tree β€” NOT []
    const res: (TreeNode | null)[] = [];
    for (let r = lo; r <= hi; r++)
      for (const L of build(lo, r - 1))
        for (const R of build(r + 1, hi))
          res.push(new TreeNode(r, L, R));
    return res;
  }
  return build(1, n);
}
⚠️ Trap Base case [null], not [] β€” with [] the inner loops never run and every tree with an empty child disappears. Output size is Catalan(n), so time is O(4ⁿ/√n) no matter what; memoizing (lo, hi) only saves constants.

4.6 Β· Easy-tier must-knows β€” write these cold

FAANG "easy" tree questions are pure recall β€” none needs a new idea, each is the Β§1 contract with a one-line combine. If the sheet is the last thing you see, these are the ones an interviewer is most likely to fire. Have every one in your fingers.

// Max Depth (104) β€” the height IS the answer
function maxDepth(root: TreeNode | null): number {
  if (!root) return 0;
  const L = maxDepth(root.left);
  const R = maxDepth(root.right);
  return 1 + Math.max(L, R);
}

// Invert (226) β€” swap each node's two children, then recurse
function invertTree(root: TreeNode | null): TreeNode | null {
  if (!root) return null;
  [root.left, root.right] = [root.right, root.left];   // 1) swap the children
  invertTree(root.left);                               // 2) invert each subtree
  invertTree(root.right);
  return root;
}

// Same Tree (100) β€” same shape AND same values
function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean {
  if (!p && !q) return true;          // both empty β†’ identical
  if (!p || !q) return false;         // exactly one empty β†’ different
  if (p.val !== q.val) return false;  // values differ here
  const L = isSameTree(p.left, q.left);
  const R = isSameTree(p.right, q.right);
  return L && R;
}

// Symmetric (101) β€” is the tree its own mirror?
function isSymmetric(root: TreeNode | null): boolean {
  function mirror(a: TreeNode | null, b: TreeNode | null): boolean {
    if (!a && !b) return true;          // both empty β†’ mirrored
    if (!a || !b) return false;         // one empty β†’ not mirrored
    if (a.val !== b.val) return false;  // values differ
    const L = mirror(a.left, b.right);    // outer ↔ outer
    const R = mirror(a.right, b.left);    // inner ↔ inner
    return L && R;
  }
  if (!root) return true;
  return mirror(root.left, root.right);
}

// Path Sum (112) β€” does ANY root-to-leaf path total `target`?
function hasPathSum(root: TreeNode | null, target: number): boolean {
  if (!root) return false;
  if (!root.left && !root.right) return root.val === target;   // a leaf
  const rest = target - root.val;
  const L = hasPathSum(root.left, rest);
  const R = hasPathSum(root.right, rest);
  return L || R;
}
🎀 Interview lens What they're really testing: that the fundamentals are automatic β€” you write these without thinking, so your head is free for the follow-up. Each is the 3-question contract with a trivial combine. Common next step: "invert, now iteratively (BFS)" Β· "same tree β†’ subtree of another (572)" Β· "path sum β†’ path sum II (113, collect every path)".

⏱️ 5 · Complexity

OperationTimeSpaceNote
Any traversal / return-value DFSO(n)O(h)h = height: O(log n) balanced, O(n) skewed
BFS level orderO(n)O(w)w = max width (up to n/2)
BST search / insert / delete / LCA-in-BSTO(h)O(h)O(log n) only if balanced
Construct / serializeO(n)O(n)each node placed once (hash map for the inorder split)
The recursion-space caveat "O(1) space" tree code still uses O(h) on the call stack. On a skewed tree that's O(n) β€” the reason deep trees want an explicit stack or Morris traversal.

⚠️ 6 Β· Pitfalls β€” and the test that catches each

BugThe input that exposes it
Confusing RETURN with RECORD (return both branches instead of one).A tree where the longest path/diameter doesn't pass through the root β†’ answer comes out too small.
Validating a BST with a local check.[5,1,4,null,null,3,6] β†’ node 3 sits in the right subtree but is < 5; a local check wrongly passes it.
Inconsistent height base case (null β†’ 0 vs leaf β†’ 0).A single node β†’ off-by-one height/diameter.
Forgetting to clamp negative child gains in max path sum.A tree with negative subtrees β†’ they drag the sum down instead of being skipped.
BFS with array.shift() β€” O(n) per dequeue.A wide tree β†’ quietly O(nΒ²); use a head index.
Recursion on a deep skewed tree.A 10⁡-node "linked-list" tree β†’ stack overflow; go iterative.
πŸ”— This pattern, elsewhere Tree recursion IS recursion β€” the return-value contract is the heart of recursion.html (your weak spot, hammered here). Iterative DFS makes the implicit stack explicit; BFS uses a queue. A BST's inorder = sorted ties to sorting; construct-from-traversals is divide & conquer. Trees are acyclic connected graphs β€” LCA and traversals generalize in graph.html; a trie is a tree too.

πŸ“Š Coverage β€” pattern β†’ LeetCode (medium & hard)

Own each pattern and these are in range. Bold = Hard tier.

PatternRepresentative LeetCodeTier
Traversal102 Level Order Β· 199 Right Side View Β· 103 Zigzag Β· 314 Vertical OrderMed
Return-value (post-order)104 Max Depth Β· 110 Balanced Β· 543 Diameter Β· 222 Count Complete Β· 1110 Delete Nodes Β· 979 Distribute Coins Β· 124 Max Path Sum Β· 968 Binary Tree CamerasEasy β†’ Hard
BST98 Validate Β· 230 Kth Smallest Β· 173 Iterator Β· 235 LCA Β· 700 Search Β· 701 Insert Β· 285 Successor Β· 450 DeleteMed
LCA236 LCA of Binary Tree Β· 1644 LCA (may not exist) Β· 437 Path Sum IIIMed
Build / serialize105/106 Build from Traversals Β· 95 Unique BSTs II (generate all) Β· 297 Serialize & DeserializeMed β†’ Hard
Easy bank104 Max Depth Β· 226 Invert Β· 100 Same Β· 101 Symmetric Β· 112 Path SumEasy

βœ… 7 Β· Self-check

Cover the right column (blurred β€” hover/tap to reveal). Say your answer out loud first.

PromptAnswer
State the 3-question contract for any tree recursion.(1) What does dfs RETURN to the parent? (2) What's the BASE CASE return? (3) How does the parent COMBINE children's returns β€” and what does it RECORD globally?
In diameter, what does dfs return vs record?Returns the height (1 + max(L,R)) for the parent; records L + R (the bending path) into a global.
Why is a local "left < node < right" check wrong for BST validation?A node must fit the range imposed by all ancestors, not just its parent β€” a far-ancestor bound can be violated while the local check passes.
What two facts make a BST powerful?Navigate by comparison (O(h)), and an inorder traversal emits values in sorted order.
In general-tree LCA, when is a node the answer?When both its left and right recursions return non-null β€” i.e. p and q are split across its two sides.
How do preorder + inorder rebuild a tree?Preorder's front is the root; its index in inorder splits the remaining values into the left and right subtrees; recurse.
What's the true space cost of "O(1)" recursive tree code?O(h) call stack β€” O(n) on a skewed tree. Use an explicit stack / Morris for deep trees.
Deleting a BST node with two children β€” what's the move?Overwrite its value with its inorder successor (smallest in the right subtree), then delete that successor β€” which has ≀1 child, an easy case.
Why does iterative BST validation track prev?A BST's inorder walk must be strictly increasing β€” each popped value must exceed the previous one; prev is that previous value.
In inorderSuccessor, when do you go left vs right?If p.val < cur.val, cur is a candidate β€” record it and go left hunting a smaller one; otherwise go right (the successor must be bigger than p).
Why does delNodes assign node.left = dfs(node.left, gone) instead of just calling it?The return value is how a deleted child gets cut out β€” it returns null and the parent overwrites its pointer. Without the assignment the parent still points at the deleted node.
In Distribute Coins, what does dfs return, and where do the moves come from?Returns the subtree's net balance val + L + R βˆ’ 1. Moves = |L| + |R| added at each node β€” every unit crossing a childβ†’parent edge costs one move.
Binary Tree Cameras β€” the three states and what null returns?0 = uncovered, 1 = has camera, 2 = covered. null β†’ 2. A child at 0 forces a camera here; a child at 1 covers me; otherwise return 0 and let the parent decide.
Why is BSTIterator.next() amortized O(1)?Each node is pushed and popped exactly once across the whole iteration β€” one call may push a long spine, but n calls do O(n) total work.

Write these cold (no peeking)

For each DFS: write the three contract lines β€” RETURN, BASE, RECORD β€” as comments before the code.