Tree TraversalDFSBFSInorderPreorderPostorderLevel Order

Tree Traversals (DFS/BFS) — The Complete Guide

Master tree traversals from first principles. Learn when to use DFS vs BFS, recognize traversal order patterns instantly, and confidently solve every tree-based interview question.

45 min read10 sections
01

Intuition from Scratch

Why does this pattern exist?

A tree is a hierarchical structure — a root node with children, each of which can have their own children, and so on. Unlike an array where you just loop from start to end, a tree branches. To process every node, you need a systematic strategy for deciding which branch to explore and when.

There are exactly two fundamental strategies: Depth-First Search (DFS) — go as deep as possible down one branch before backtracking, and Breadth-First Search (BFS) — visit all nodes at the current depth before moving deeper. Every tree problem uses one of these two strategies (or a variation of them).

DFS itself has three flavors depending on when you process the current node relative to its children: preorder (process before children), inorder (process between left and right child), and postorder (process after children). Each order gives you different information about the tree.

Analogies to Build Intuition

🏢

Exploring a Building (DFS vs BFS)

DFS: You enter the building, take the stairs all the way to the top floor, explore every room on that floor, then come back down one floor at a time. BFS: You explore every room on floor 1 first, then every room on floor 2, then floor 3, and so on. DFS goes deep first; BFS goes wide first.

📂

File System Navigation

Your computer's file system is a tree. DFS is like opening a folder, then opening a subfolder inside it, then a subfolder inside that — going as deep as possible before backing out. BFS is like listing all files and folders at the current level before entering any subfolder.

👨‍👩‍👧‍👦

Family Tree — When Do You Introduce Yourself?

Preorder: You introduce yourself, then introduce your children. Inorder: You introduce your left child, then yourself, then your right child (this gives sorted order in a BST). Postorder: You let all your children introduce themselves first, then you go last (useful when you need children's results to compute your own).

What kind of problems does it solve?

Tree traversals are your go-to when:

  • You need to visit every node in a tree (height, size, sum, validation)
  • You need to process nodes level by level (level order, zigzag, right side view)
  • You need to compute bottom-up results (height, diameter, subtree sums)
  • You need to serialize or reconstruct a tree from traversal orders
  • You need to find paths (root-to-leaf, path sum, lowest common ancestor)
  • You need sorted order from a BST (inorder traversal)

The TreeNode — Your Building Block

TreeNode Definitiontypescript
class TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;

  constructor(
    val: number = 0,
    left: TreeNode | null = null,
    right: TreeNode | null = null
  ) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

// Example tree:
//       1
//      / \
//     2   3
//    / \
//   4   5
// Preorder:  [1, 2, 4, 5, 3]  — root first
// Inorder:   [4, 2, 5, 1, 3]  — left, root, right
// Postorder: [4, 5, 2, 3, 1]  — children first, root last
// Level order: [1, 2, 3, 4, 5] — top to bottom, left to right

The core insight

Every tree problem is a traversal problem in disguise. The question is always: (1) Which traversal order do I need? (2) What do I compute at each node? (3) Do I need information from children (bottom-up / postorder) or from the parent (top-down / preorder)? Answer these three questions and the solution writes itself.

02

Pattern Recognition

The problem will give you a TreeNode. The real question is: which traversal strategy and which order? Here are the signals.

DFS vs BFS — When to Use Which

Use DFS when you see...

  • "Height" or "depth" of the tree
  • "Path from root to leaf" or "path sum"
  • "Validate BST" or "sorted order"
  • "Diameter" or "longest path between two nodes"
  • "Lowest common ancestor"
  • "Serialize / deserialize" a tree
  • "Subtree" problems (is subtree, count nodes in subtree)
  • "Flatten tree to linked list"
  • Bottom-up computation (need children's results first)
  • The tree could be very wide but not very deep

Use BFS when you see...

  • "Level order traversal" or "by level"
  • "Right side view" or "left side view"
  • "Zigzag" or "spiral" level order
  • "Minimum depth" (shortest root-to-leaf path)
  • "Connect next pointers" at same level
  • "Average of levels" or "largest value in each row"
  • "Cousins" in a binary tree
  • You need to process nodes level by level
  • You need the shortest path in an unweighted tree
  • The tree could be very deep but not very wide

Which DFS Order? — Preorder vs Inorder vs Postorder

OrderProcess WhenUse ForClassic Example
Preorder (root → left → right)Before visiting childrenCopying/serializing tree structure, top-down passing info to childrenSerialize tree, flatten to linked list
Inorder (left → root → right)Between left and right childBST problems — gives sorted order. Kth smallest, validate BSTKth Smallest in BST, Validate BST
Postorder (left → right → root)After visiting both childrenBottom-up computation — need children's results first. Height, diameter, subtree sumMax depth, diameter, balanced tree check

Tree Traversals vs. Similar Patterns

PatternWhen to UseKey Difference
Tree DFSHierarchical structure, path problems, bottom-up computationExplores depth-first; uses recursion or explicit stack
Tree BFSLevel-by-level processing, shortest path in unweighted treeExplores breadth-first; uses a queue
Graph DFS/BFSCycles possible, multiple paths between nodesNeeds a visited set; trees don't have cycles
BacktrackingNeed all paths/combinations, with pruningDFS + undo choices; tree traversal usually doesn't undo
Divide & ConquerSplit problem into independent subproblemsSimilar to postorder DFS but emphasizes combining results from halves

The quick decision

Need level-by-level info? → BFS. Need sorted order from BST? → Inorder DFS. Need children's results to compute parent's? → Postorder DFS. Need to pass info from parent to children? → Preorder DFS. When in doubt, start with recursive DFS — it's the most natural and covers most tree problems.

03

Brute Force → Optimized Thinking

Let's walk through the thinking evolution using a classic example: Maximum Depth of Binary Tree — given the root, return the tree's maximum depth (number of nodes along the longest root-to-leaf path).

1

Brute Force — Enumerate All Root-to-Leaf Paths

O(n) time · O(n) space

Find every root-to-leaf path, track each path's length, return the maximum. This works but requires storing all paths explicitly, which is wasteful.

Brute Force — Collect All Pathstypescript
function maxDepth(root: TreeNode | null): number {
  const paths: number[][] = [];

  function collectPaths(node: TreeNode | null, path: number[]) {
    if (!node) return;
    path.push(node.val);
    if (!node.left && !node.right) {
      paths.push([...path]); // leaf — save the path
    }
    collectPaths(node.left, path);
    collectPaths(node.right, path);
    path.pop(); // backtrack
  }

  collectPaths(root, []);
  return Math.max(0, ...paths.map(p => p.length));
}
// Problem: We're storing entire paths just to get their lengths.
// We don't need the actual paths — just the maximum length.
2

Better — Track Depth During Traversal (Top-Down)

O(n) time · O(h) space

Instead of storing paths, pass the current depth as a parameter. At each leaf, update a global maximum. This is a preorder (top-down) approach — the parent tells the child its depth.

Top-Down — Pass Depth as Parametertypescript
function maxDepth(root: TreeNode | null): number {
  let maxD = 0;

  function dfs(node: TreeNode | null, depth: number) {
    if (!node) return;
    depth++;
    if (!node.left && !node.right) {
      maxD = Math.max(maxD, depth); // leaf — update max
    }
    dfs(node.left, depth);
    dfs(node.right, depth);
  }

  dfs(root, 0);
  return maxD;
}
// Better! No path storage. But we're using a global variable.
// Can we make it even cleaner?
3

Optimal — Bottom-Up Recursion (Postorder)

O(n) time · O(h) space

The cleanest approach: each node asks its children 'how deep are you?' and returns 1 + max(left, right). This is postorder — children report their results up to the parent. No global variables, no path tracking. Pure recursion.

Bottom-Up — Postorder (Optimal)typescript
function maxDepth(root: TreeNode | null): number {
  if (!root) return 0;
  const left = maxDepth(root.left);
  const right = maxDepth(root.right);
  return 1 + Math.max(left, right);
}
// 3 lines. O(n) time, O(h) space (recursion stack).
// Each node computes its answer from its children's answers.
// This is the "postorder" pattern — the most common tree DFS pattern.

Why Does Each Optimization Work?

1

Brute → Top-Down

We realized we don't need the actual paths — just the depth. By passing depth as a parameter (top-down / preorder), we eliminate path storage entirely.

2

Top-Down → Bottom-Up

We realized the problem has optimal substructure: the depth of a tree = 1 + max(depth of left subtree, depth of right subtree). This naturally maps to postorder recursion where children return their results to the parent. No global state needed.

3

The General Principle

Most tree problems follow this pattern: if you need information from children to compute the parent's answer, use postorder (bottom-up). If you need to pass information from parent to children, use preorder (top-down). Recognizing which direction information flows is the key skill.

Top-down vs bottom-up — the key question

Ask yourself: "Does the parent need to tell the child something (like its depth or path so far)?" → Top-down / preorder. "Does the parent need to know something about its children (like their height or sum)?" → Bottom-up / postorder. Many problems can be solved either way, but one direction is usually cleaner.

04

Core Templates

These five templates cover virtually every tree traversal problem. Memorize the structure — the only thing that changes between problems is what you compute at each node.

Template 1: Recursive DFS (Postorder — Bottom-Up)

The most common tree pattern. Each node computes its answer from its children's answers. Used for height, diameter, subtree sum, balance check.

Postorder DFS Templatetypescript
function solve(root: TreeNode | null): ResultType {
  // Base case: empty tree
  if (!root) return baseValue;

  // Recurse on children FIRST (postorder)
  const leftResult = solve(root.left);
  const rightResult = solve(root.right);

  // Compute current node's answer using children's results
  const currentResult = combine(leftResult, rightResult, root.val);

  // Optionally update a global answer (e.g., diameter)
  globalAnswer = Math.max(globalAnswer, someComputation);

  return currentResult;
}
// Examples:
// Height: return 1 + Math.max(left, right)
// Size:   return 1 + left + right
// Sum:    return root.val + left + right

Template 2: Recursive DFS (Preorder — Top-Down)

The parent passes information down to children. Used for path tracking, depth computation, serialization, and tree construction.

Preorder DFS Templatetypescript
function solve(node: TreeNode | null, parentInfo: InfoType): void {
  if (!node) return;

  // Process current node FIRST (preorder)
  // Use parentInfo to compute something for this node
  doSomething(node, parentInfo);

  // Pass updated info to children
  solve(node.left, updatedInfo);
  solve(node.right, updatedInfo);
}
// Examples:
// Path sum: pass remaining target down, check at leaf
// Depth:    pass current depth, increment for children
// Flatten:  process node, then recurse (careful with pointer rewiring)

Template 3: Inorder DFS (BST Sorted Order)

Visits nodes in sorted order for a BST. Used for kth smallest, validate BST, convert BST to sorted list.

Inorder DFS Templatetypescript
function inorder(root: TreeNode | null): void {
  if (!root) return;

  inorder(root.left);       // visit left subtree

  // Process current node — this happens in SORTED ORDER for BST
  process(root.val);

  inorder(root.right);      // visit right subtree
}

// Iterative version (useful when you need to stop early):
function inorderIterative(root: TreeNode | null): number[] {
  const result: number[] = [];
  const stack: TreeNode[] = [];
  let curr = root;

  while (curr || stack.length) {
    // Go as far left as possible
    while (curr) {
      stack.push(curr);
      curr = curr.left;
    }
    // Process the node
    curr = stack.pop()!;
    result.push(curr.val);
    // Move to right subtree
    curr = curr.right;
  }

  return result;
}
// The iterative version is important for "kth smallest" where
// you want to stop after k nodes without visiting the entire tree.

Template 4: BFS (Level Order Traversal)

Process nodes level by level using a queue. Used for level order, right side view, zigzag, minimum depth, and connecting next pointers.

BFS Level Order Templatetypescript
function bfs(root: TreeNode | null): ResultType {
  if (!root) return baseValue;

  const queue: TreeNode[] = [root];
  const result: number[][] = [];

  while (queue.length) {
    const levelSize = queue.length;  // nodes at current level
    const currentLevel: number[] = [];

    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()!;
      currentLevel.push(node.val);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    result.push(currentLevel);
  }

  return result;
}
// Key: capture queue.length BEFORE the inner loop.
// This tells you exactly how many nodes are at the current level.
// Variations:
// Right side view: take the LAST element of each level
// Zigzag: reverse every other level
// Min depth: return when you hit the first leaf

Template 5: DFS with Global State (Diameter Pattern)

Some problems need to compute something at each node AND track a global optimum across all nodes. The function returns one thing (for the parent) but updates a separate global answer.

DFS + Global State Templatetypescript
function diameterOfBinaryTree(root: TreeNode | null): number {
  let maxDiameter = 0;

  function height(node: TreeNode | null): number {
    if (!node) return 0;

    const left = height(node.left);
    const right = height(node.right);

    // Update global answer: diameter through this node
    maxDiameter = Math.max(maxDiameter, left + right);

    // Return height (what the parent needs)
    return 1 + Math.max(left, right);
  }

  height(root);
  return maxDiameter;
}
// The function RETURNS height (for parent's computation)
// but UPDATES diameter (the actual answer) as a side effect.
// This "return one thing, update another" pattern is very common:
// - Diameter: return height, update max path length
// - Max path sum: return max single-branch sum, update max any-path sum
// - Balanced tree: return height, update isBalanced flag

Which template to pick?

(1) Need children's results? → Template 1 (postorder). (2) Need to pass info down? → Template 2 (preorder). (3) Need sorted order from BST? → Template 3 (inorder). (4) Need level-by-level? → Template 4 (BFS). (5) Need to track a global optimum across all nodes? → Template 5 (DFS + global). Many hard problems combine templates (e.g., postorder + global state).

05

Variations & Sub-Patterns

Tree traversal is a family of techniques. Here are the most important variations and how the approach changes for each.

VariationTraversal TypeClassic Example
Max / Min DepthPostorder DFS (bottom-up) or BFS (min depth)Maximum Depth (LC 104), Minimum Depth (LC 111)
Path SumPreorder DFS — pass remaining sum down, check at leafPath Sum (LC 112), Path Sum II (LC 113)
Diameter / Longest PathPostorder + global state — return height, update max pathDiameter of Binary Tree (LC 543)
Validate BSTInorder DFS (check sorted) or preorder with min/max boundsValidate BST (LC 98)
Level Order & VariantsBFS with level-size trackingLevel Order (LC 102), Zigzag (LC 103), Right Side View (LC 199)
Lowest Common AncestorPostorder — if left and right both return non-null, current is LCALCA of Binary Tree (LC 236)
Serialize / DeserializePreorder DFS — encode structure with null markersSerialize and Deserialize (LC 297)
Construct from TraversalsPreorder gives root, inorder gives left/right splitConstruct from Preorder & Inorder (LC 105)
Balanced Tree CheckPostorder + early termination — return -1 if unbalancedBalanced Binary Tree (LC 110)
Flatten to Linked ListReverse postorder (right → left → root) or Morris traversalFlatten Binary Tree (LC 114)

Deep Dive: Lowest Common Ancestor (LCA)

LCA is one of the most asked tree problems. The idea: do a postorder traversal. If the current node is one of the targets, return it. If the left subtree returns one target and the right subtree returns the other, the current node is the LCA.

Problems mentioned above

Lowest Common Ancestortypescript
function lowestCommonAncestor(
  root: TreeNode | null,
  p: TreeNode,
  q: TreeNode
): TreeNode | null {
  // Base cases
  if (!root) return null;
  if (root === p || root === q) return root;

  // Postorder: search both subtrees
  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);

  // If both subtrees found a target, current node is the LCA
  if (left && right) return root;

  // Otherwise, return whichever side found something
  return left ?? right;
}
// Why postorder? We need BOTH children's results before deciding.
// If left returns p and right returns q → root is LCA.
// If only one side returns non-null → LCA is deeper in that subtree.

Deep Dive: Construct Tree from Traversals

Given preorder and inorder arrays, reconstruct the tree. Preorder's first element is always the root. Find that root in inorder — everything to its left is the left subtree, everything to its right is the right subtree. Recurse.

Construct from Preorder & Inordertypescript
function buildTree(
  preorder: number[],
  inorder: number[]
): TreeNode | null {
  // Map inorder values to indices for O(1) lookup
  const inMap = new Map<number, number>();
  inorder.forEach((val, i) => inMap.set(val, i));

  let preIdx = 0;

  function build(inLeft: number, inRight: number): TreeNode | null {
    if (inLeft > inRight) return null;

    const rootVal = preorder[preIdx++];
    const root = new TreeNode(rootVal);
    const inIdx = inMap.get(rootVal)!;

    root.left = build(inLeft, inIdx - 1);   // left subtree
    root.right = build(inIdx + 1, inRight);  // right subtree

    return root;
  }

  return build(0, inorder.length - 1);
}
// Key insight: preorder gives us roots in order (root, then left subtree
// roots, then right subtree roots). Inorder tells us the split point.
// The hash map makes root lookup O(1) instead of O(n).
06

Visual Walkthrough

Let's trace through the three DFS orders and BFS on the same tree to see exactly how each one visits nodes.

All Four Traversals — Same Treetext
Tree:
        1
       / \
      2   3
     / \   \
    4   5   6

═══════════════════════════════════════════════════
PREORDER (rootleftright) — "I go first"
═══════════════════════════════════════════════════
Visit 1go left
  Visit 2go left
    Visit 4no children, backtrack
  back to 2go right
    Visit 5no children, backtrack
  back to 2, backtrack
back to 1go right
  Visit 3go right
    Visit 6no children, backtrack
  back to 3, backtrack

Result: [1, 2, 4, 5, 3, 6]
Use: Serialization, copying tree structure

═══════════════════════════════════════════════════
INORDER (leftrootright) — "Left child goes first"
═══════════════════════════════════════════════════
Go left from 1go left from 2
  Visit 4 (no left child)
Visit 2
  Go right from 2Visit 5
Visit 1
  Go right from 1go left from 3 (no left)
  Visit 3
    Go rightVisit 6

Result: [4, 2, 5, 1, 3, 6]
Use: Sorted order in BST

═══════════════════════════════════════════════════
POSTORDER (leftrightroot) — "Children go first"
═══════════════════════════════════════════════════
Go left from 1go left from 2
  Visit 4 (leaf)
  Go right from 2Visit 5 (leaf)
Visit 2 (both children done)
  Go right from 1go right from 3
    Visit 6 (leaf)
  Visit 3 (children done)
Visit 1 (both children done)

Result: [4, 5, 2, 6, 3, 1]
Use: Height, diameter, delete tree (children before parent)

═══════════════════════════════════════════════════
BFS / LEVEL ORDER"Floor by floor"
═══════════════════════════════════════════════════
Queue: [1]
Level 0: process 1, enqueue 2, 3     → [1]
Queue: [2, 3]
Level 1: process 2, enqueue 4, 5
         process 3, enqueue 6        → [2, 3]
Queue: [4, 5, 6]
Level 2: process 4, 5, 6             → [4, 5, 6]

Result: [[1], [2, 3], [4, 5, 6]]
Use: Level-by-level processing, right side view, min depth

Diameter of Binary Tree — Postorder + Global State Trace

Diameter Tracetext
Tree:
        1
       / \
      2   3
     / \
    4   5

height(4) → left=0, right=0diameter through 4 = 0+0 = 0return 1
height(5) → left=0, right=0diameter through 5 = 0+0 = 0return 1
height(2) → left=1, right=1diameter through 2 = 1+1 = 2return 2
height(3) → left=0, right=0diameter through 3 = 0+0 = 0return 1
height(1) → left=2, right=1diameter through 1 = 2+1 = 3 ★ → return 3

maxDiameter = 3 (path: 4213)

Key insight: The function RETURNS height (for the parent to use)
but UPDATES maxDiameter (the actual answer) at every node.
The diameter through any node = leftHeight + rightHeight.
The answer is the maximum diameter across ALL nodes.

Validate BST — Preorder with Bounds Trace

Validate BST Tracetext
Tree:
        5
       / \
      3   7
     / \   \
    1   4   9

validate(5, -∞, +∞) → 5 is in range
  validate(3, -∞, 5) → 3 is in range
    validate(1, -∞, 3) → 1 is in range
      validate(null) → true (base case)
      validate(null) → true
    validate(4, 3, 5) → 4 is in range
      validate(null) → true
      validate(null) → true
  validate(7, 5, +∞) → 7 is in range
    validate(null) → true
    validate(9, 7, +∞) → 9 is in range
      validate(null) → true
      validate(null) → true

All nodes validreturn true

This is PREORDER with bounds: parent passes [min, max] range
to children. Left child gets [min, parent.val]. Right child
gets [parent.val, max]. If any node is outside its rangefalse.
07

Practice Problems

These 7 problems are carefully ordered from easy to hard. Each one teaches a different traversal technique. Solve them in order.

1

LC 104. Maximum Depth of Binary Tree

Easy

Why this pattern:

The simplest postorder (bottom-up) problem. Each node's depth = 1 + max(left depth, right depth). This is the foundation for every bottom-up tree computation.

Key idea: Base case: null → 0. Recursive case: 1 + Math.max(maxDepth(left), maxDepth(right)). Three lines of code. Practice until this is automatic — it's the building block for diameter, balanced check, and more.

2

LC 226. Invert Binary Tree

Easy

Why this pattern:

Preorder or postorder — swap left and right children at every node. Tests your understanding that tree operations are just 'do something at each node during traversal.' Order doesn't matter here.

Key idea: At each node: swap left and right children, then recurse on both. Can be done preorder (swap then recurse) or postorder (recurse then swap). Both work because swapping is independent of child structure.

3

LC 102. Binary Tree Level Order Traversal

Medium

Why this pattern:

The canonical BFS problem. Process nodes level by level using a queue. The key trick: capture queue.length before the inner loop to know how many nodes are at the current level.

Key idea: Use a queue. At each level, record queue.length (= number of nodes at this level). Process exactly that many nodes, enqueueing their children. Each iteration of the outer loop = one level.

4

LC 98. Validate Binary Search Tree

Medium

Why this pattern:

Two approaches: (1) Preorder with bounds — pass [min, max] range down, each node must be within its range. (2) Inorder — values must be strictly increasing. Both test different traversal skills.

Key idea: Preorder approach: validate(node, min, max). Left child gets [min, node.val], right child gets [node.val, max]. If node.val is outside [min, max], return false. Start with [-Infinity, Infinity].

5

LC 236. Lowest Common Ancestor

Medium

Why this pattern:

Postorder — search both subtrees, then combine results. If both subtrees return non-null, the current node is the LCA. If only one returns non-null, propagate it up.

Key idea: Base: null → null, node is p or q → return node. Recurse left and right. If both non-null → current is LCA. If one is null → return the other. The postorder structure naturally bubbles the answer up.

6

LC 543. Diameter of Binary Tree

Medium

Why this pattern:

Postorder + global state (Template 5). The function returns height for the parent, but updates a global maxDiameter at each node. Diameter through a node = leftHeight + rightHeight.

Key idea: At each node: compute left and right heights. Diameter through this node = left + right. Update global max. Return 1 + max(left, right) as height for the parent. The 'return one thing, update another' pattern.

7

LC 297. Serialize and Deserialize Binary Tree

Hard

Why this pattern:

Preorder DFS with null markers. Serialize: visit each node, write its value (or 'null' for empty nodes). Deserialize: read values in the same preorder sequence, recursively build the tree.

Key idea: Serialize: preorder traversal, append val or 'null' to a string. Deserialize: split the string, use a pointer/index that advances as you consume values. Recursively build: read value → create node → build left → build right.

Practice strategy

For each problem: (1) Identify the traversal type (DFS preorder/inorder/postorder or BFS). (2) Determine the direction of information flow (top-down or bottom-up). (3) Write the base case first. (4) After solving, write: "This is [traversal type] because [information flows in this direction]."

08

Common Mistakes

These are the traps that catch people on tree traversal problems. Learn them here so you don't learn them in an interview.

🔄

Using the wrong traversal order

You use preorder when you need postorder (or vice versa). For example, trying to compute height top-down instead of bottom-up, leading to convoluted code with extra parameters.

Ask: 'Does the parent need children's results?' → postorder. 'Does the child need parent's info?' → preorder. 'Do I need sorted BST order?' → inorder. 'Level by level?' → BFS. Get this right BEFORE coding.

🚫

Forgetting the base case for null nodes

You handle leaf nodes but forget that null children also need a base case. This causes null pointer errors on trees with nodes that have only one child.

ALWAYS start with: if (!node) return baseValue. This handles empty trees, null children, and leaf nodes' children all at once. The base case for null is the foundation of every tree recursion.

📏

Confusing height vs depth

Height is bottom-up (leaf = 0 or 1, root = max). Depth is top-down (root = 0 or 1, leaf = max). Mixing them up gives off-by-one errors or wrong traversal direction.

Height = 'how tall is the subtree below me?' → postorder (ask children). Depth = 'how far am I from the root?' → preorder (parent tells me). Be explicit about which one the problem asks for.

🌳

Validating BST by only checking immediate children

You check node.left.val < node.val and node.right.val > node.val, but this misses cases where a deeper node violates the BST property (e.g., a node in the left subtree is greater than the root).

Pass min/max bounds down the tree. Left child must be in [min, node.val), right child must be in (node.val, max]. Or use inorder traversal and verify the sequence is strictly increasing.

📊

Not capturing level size in BFS

You process the queue without tracking how many nodes belong to the current level. This makes it impossible to group nodes by level or know when a level ends.

At the start of each outer loop iteration: const levelSize = queue.length. Then process exactly levelSize nodes in the inner loop. This is the critical BFS trick — without it, you can't do level-order grouping.

Recomputing height multiple times (O(n²) trap)

In balanced tree check, you call height() for each node AND recursively call isBalanced() — computing height from scratch at every level. This is O(n²) instead of O(n).

Combine the check into a single postorder traversal. Return -1 to signal 'unbalanced' instead of a separate boolean. Each node is visited once → O(n). This is the 'early termination with sentinel return value' pattern.

09

Interview Strategy

Tree problems are among the most common in FAANG interviews. They test recursion, problem decomposition, and your ability to think about information flow. Here's how to approach them.

The 5-Step Interview Flow

1

Clarify the Tree Type

'Is this a binary tree or n-ary? Is it a BST? Can it have duplicate values? Can it be empty?' These determine which traversal and which optimizations apply. BST → inorder gives sorted order. N-ary → loop over children instead of left/right.

2

Identify Information Flow Direction

'Do I need children's results to compute the parent's answer? Or do I need to pass information from parent to children?' This determines preorder vs postorder. Say it explicitly: 'I need bottom-up computation, so I'll use postorder DFS.'

3

Write the Base Case First

Start with: 'if (!node) return [base value]'. This anchors your recursion. Then write the recursive case. For BFS, start with the queue initialization and the level-size pattern.

4

Code the Recursion / BFS Loop

Keep it clean. Name your recursive function clearly (dfs, height, validate). For the 'return one thing, update another' pattern, explain what the function returns vs what it updates globally.

5

Test with Edge Cases

Trace through: (1) empty tree (null), (2) single node, (3) skewed tree (all left or all right — this is the worst case for recursion depth), (4) complete/balanced tree. State the complexity: O(n) time, O(h) space where h = height.

Common Follow-Up Questions

Follow-UpHow to Handle
"Can you do it iteratively?"DFS: use an explicit stack. Preorder is easiest (push right then left). Inorder: go-left loop + stack. Postorder: two-stack method or reverse of modified preorder. BFS is already iterative.
"What if the tree is very deep (stack overflow)?"Convert recursion to iterative with an explicit stack. Mention that the recursive approach uses O(h) stack space, which is O(n) for a skewed tree. Iterative uses heap memory instead.
"Can you do it in O(1) space?"Morris Traversal: temporarily modify the tree's right pointers to create a threaded tree. Achieves O(1) space for inorder/preorder. Mention it as an advanced technique — interviewers are impressed you know it exists.
"What if it's an n-ary tree?"Replace left/right with a loop over node.children. The traversal logic is identical — just iterate over all children instead of two. BFS works the same way.
"Extend to find all paths / all ancestors?"Use backtracking: add current node to path before recursing, remove it after. For ancestors, the LCA pattern generalizes — return information up the recursion stack.
"What's the time and space complexity?"Time: O(n) — every node visited once. Space: O(h) for DFS (recursion stack), O(w) for BFS (queue width, max = n/2 for complete tree). h = log n for balanced, n for skewed.

The meta-skill

Tree problems test your ability to decompose a problem recursively. The interviewer wants to see: (1) Can you identify the right traversal order? (2) Can you define what each node computes from its children? (3) Can you handle edge cases (null, single node, skewed)? Practice thinking out loud: "Each node needs to know its children's heights, so I'll use postorder. The base case for null is 0. Each node returns 1 + max(left, right)."

10

Summary & Cheat Sheet

Pin this section. Review it before mock interviews and contests.

Quick Revision Cheat Sheet

Core idea: Every tree problem = choose a traversal order + decide what to compute at each node

DFS preorder: Process node BEFORE children. Top-down: pass info from parent to child. Serialize, path tracking

DFS inorder: Process node BETWEEN children. Gives SORTED order for BST. Validate BST, kth smallest

DFS postorder: Process node AFTER children. Bottom-up: compute from children's results. Height, diameter, LCA

BFS: Queue + level-size trick. Level order, right side view, zigzag, min depth

Global state pattern: Function RETURNS one thing (height) but UPDATES another (diameter). Postorder + side effect

Base case: Always: if (!node) return baseValue. Handles null, empty tree, and leaf children

BST validation: Pass [min, max] bounds preorder. Left: [min, node.val). Right: (node.val, max]. Or inorder → check sorted

LCA: Postorder: if left and right both non-null → current is LCA. Otherwise propagate the non-null one up

Construct tree: Preorder[0] = root. Find root in inorder → left of it = left subtree, right = right subtree. Recurse

Complexity: Time: O(n) always. Space: O(h) for DFS, O(w) for BFS. h = log n balanced, n skewed

Mental trigger: See TreeNode → ask: preorder/inorder/postorder/BFS? Top-down or bottom-up? What does each node compute?

Decision Flowchart

Which Tree Traversal? — Decision Treetext
What does the problem ask?
├── Process nodes level by level?
│   → BFS (queue + level-size trick)
│   ├── Right/left side viewlast/first node per level
│   ├── Zigzagreverse every other level
│   └── Min depthreturn at first leaf encountered
├── Need sorted order from BST?
│   → Inorder DFS (leftrootright)
│   ├── Kth smalleststop after k nodes (iterative preferred)
│   └── Validate BSTcheck strictly increasing sequence
├── Need children's results to compute parent's answer?
│   → Postorder DFS (bottom-up)
│   ├── Height / depth1 + max(left, right)
│   ├── Diameterreturn height, update global max(left + right)
│   ├── Balanced checkreturn -1 as sentinel for unbalanced
│   ├── LCAif both sides non-null, current is LCA
│   └── Subtree sumroot.val + left + right
├── Need to pass info from parent to children?
│   → Preorder DFS (top-down)
│   ├── Path sumpass remaining target, check at leaf
│   ├── BST validationpass [min, max] bounds
│   ├── Serializewrite node value, recurse children
│   └── Flattenprocess node, then recurse
└── Not sure?
Default to postorder DFS. Most tree problems are bottom-up.
      If that feels wrong, try preorder or BFS.

One last thing

Tree traversals are the single most tested topic in coding interviews. The good news: there are only 4 traversal types and 5 templates. Once you internalize the "which direction does information flow?" question, every tree problem becomes a fill-in-the-blank exercise. Solve the 7 problems in Section 07, and you'll handle any tree question with confidence.