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.
Table of Contents
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
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.
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
| Order | Process When | Use For | Classic Example |
|---|---|---|---|
| Preorder (root → left → right) | Before visiting children | Copying/serializing tree structure, top-down passing info to children | Serialize tree, flatten to linked list |
| Inorder (left → root → right) | Between left and right child | BST problems — gives sorted order. Kth smallest, validate BST | Kth Smallest in BST, Validate BST |
| Postorder (left → right → root) | After visiting both children | Bottom-up computation — need children's results first. Height, diameter, subtree sum | Max depth, diameter, balanced tree check |
Tree Traversals vs. Similar Patterns
| Pattern | When to Use | Key Difference |
|---|---|---|
| Tree DFS | Hierarchical structure, path problems, bottom-up computation | Explores depth-first; uses recursion or explicit stack |
| Tree BFS | Level-by-level processing, shortest path in unweighted tree | Explores breadth-first; uses a queue |
| Graph DFS/BFS | Cycles possible, multiple paths between nodes | Needs a visited set; trees don't have cycles |
| Backtracking | Need all paths/combinations, with pruning | DFS + undo choices; tree traversal usually doesn't undo |
| Divide & Conquer | Split problem into independent subproblems | Similar 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.
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).
Brute Force — Enumerate All Root-to-Leaf Paths
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.
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.
Better — Track Depth During Traversal (Top-Down)
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.
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?
Optimal — Bottom-Up Recursion (Postorder)
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.
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?
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.
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.
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.
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.
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.
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.
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.
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.
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).
Variations & Sub-Patterns
Tree traversal is a family of techniques. Here are the most important variations and how the approach changes for each.
| Variation | Traversal Type | Classic Example |
|---|---|---|
| Max / Min Depth | Postorder DFS (bottom-up) or BFS (min depth) | Maximum Depth (LC 104), Minimum Depth (LC 111) |
| Path Sum | Preorder DFS — pass remaining sum down, check at leaf | Path Sum (LC 112), Path Sum II (LC 113) |
| Diameter / Longest Path | Postorder + global state — return height, update max path | Diameter of Binary Tree (LC 543) |
| Validate BST | Inorder DFS (check sorted) or preorder with min/max bounds | Validate BST (LC 98) |
| Level Order & Variants | BFS with level-size tracking | Level Order (LC 102), Zigzag (LC 103), Right Side View (LC 199) |
| Lowest Common Ancestor | Postorder — if left and right both return non-null, current is LCA | LCA of Binary Tree (LC 236) |
| Serialize / Deserialize | Preorder DFS — encode structure with null markers | Serialize and Deserialize (LC 297) |
| Construct from Traversals | Preorder gives root, inorder gives left/right split | Construct from Preorder & Inorder (LC 105) |
| Balanced Tree Check | Postorder + early termination — return -1 if unbalanced | Balanced Binary Tree (LC 110) |
| Flatten to Linked List | Reverse postorder (right → left → root) or Morris traversal | Flatten 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
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.
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).
Visual Walkthrough
Let's trace through the three DFS orders and BFS on the same tree to see exactly how each one visits nodes.
Tree: 1 / \ 2 3 / \ \ 4 5 6 ═══════════════════════════════════════════════════ PREORDER (root → left → right) — "I go first" ═══════════════════════════════════════════════════ Visit 1 → go left Visit 2 → go left Visit 4 → no children, backtrack back to 2 → go right Visit 5 → no children, backtrack back to 2, backtrack back to 1 → go right Visit 3 → go right Visit 6 → no children, backtrack back to 3, backtrack Result: [1, 2, 4, 5, 3, 6] Use: Serialization, copying tree structure ═══════════════════════════════════════════════════ INORDER (left → root → right) — "Left child goes first" ═══════════════════════════════════════════════════ Go left from 1 → go left from 2 Visit 4 (no left child) Visit 2 Go right from 2 → Visit 5 Visit 1 Go right from 1 → go left from 3 (no left) Visit 3 Go right → Visit 6 Result: [4, 2, 5, 1, 3, 6] Use: Sorted order in BST ═══════════════════════════════════════════════════ POSTORDER (left → right → root) — "Children go first" ═══════════════════════════════════════════════════ Go left from 1 → go left from 2 Visit 4 (leaf) Go right from 2 → Visit 5 (leaf) Visit 2 (both children done) Go right from 1 → go 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
Tree: 1 / \ 2 3 / \ 4 5 height(4) → left=0, right=0 → diameter through 4 = 0+0 = 0 → return 1 height(5) → left=0, right=0 → diameter through 5 = 0+0 = 0 → return 1 height(2) → left=1, right=1 → diameter through 2 = 1+1 = 2 → return 2 height(3) → left=0, right=0 → diameter through 3 = 0+0 = 0 → return 1 height(1) → left=2, right=1 → diameter through 1 = 2+1 = 3 ★ → return 3 maxDiameter = 3 (path: 4 → 2 → 1 → 3) 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
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 valid → return 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 range → false.
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different traversal technique. Solve them in order.
LC 104. Maximum Depth of Binary Tree
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.
LC 226. Invert Binary Tree
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.
LC 102. Binary Tree Level Order Traversal
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.
LC 98. Validate Binary Search Tree
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].
LC 236. Lowest Common Ancestor
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.
LC 543. Diameter of Binary Tree
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.
LC 297. Serialize and Deserialize Binary Tree
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]."
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.
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
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.
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.'
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.
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.
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-Up | How 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)."
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
What does the problem ask? ├── Process nodes level by level? │ → BFS (queue + level-size trick) │ ├── Right/left side view → last/first node per level │ ├── Zigzag → reverse every other level │ └── Min depth → return at first leaf encountered ├── Need sorted order from BST? │ → Inorder DFS (left → root → right) │ ├── Kth smallest → stop after k nodes (iterative preferred) │ └── Validate BST → check strictly increasing sequence ├── Need children's results to compute parent's answer? │ → Postorder DFS (bottom-up) │ ├── Height / depth → 1 + max(left, right) │ ├── Diameter → return height, update global max(left + right) │ ├── Balanced check → return -1 as sentinel for unbalanced │ ├── LCA → if both sides non-null, current is LCA │ └── Subtree sum → root.val + left + right ├── Need to pass info from parent to children? │ → Preorder DFS (top-down) │ ├── Path sum → pass remaining target, check at leaf │ ├── BST validation → pass [min, max] bounds │ ├── Serialize → write node value, recurse children │ └── Flatten → process 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.