Trees (DFS)
Explore binary trees using depth-first search. Master recursive traversals (preorder, inorder, postorder), state passing through call stacks, bottom-up computation, and iterative DFS with an explicit stack.
Learn & Reference
Understanding the Pattern
Trees (DFS) Pattern
A binary tree is a hierarchical data structure where each node has at most two children: left and right. Depth-first search (DFS) explores as deep as possible along each branch before backtracking. Because trees are recursive structures — every subtree is itself a tree — recursion is the natural tool for DFS.
Core Concept
Every node has three fields: val (the data), left (pointer to the left child or null), and right (pointer to the right child or null). The tree is accessed through its root pointer.
1
/ \
2 3
/ \ \
4 5 6
The key insight: any property of a tree can be computed by combining the answers from its left and right subtrees. This recursive decomposition is the foundation of nearly every DFS solution.
Key Techniques
1. Recursive Preorder, Inorder, and Postorder Traversal
The three classic DFS orderings differ only in when you process the current node relative to its children:
- Preorder (root, left, right): Process the node first, then recurse left, then right. Used for serialization and copying trees.
- Inorder (left, root, right): Recurse left, process the node, then recurse right. On a BST, this visits nodes in sorted order.
- Postorder (left, right, root): Recurse left, recurse right, then process the node. Used when you need children's results before computing the parent's (e.g., computing height).
Tree: 1
/ \
2 3
Preorder: [1, 2, 3]
Inorder: [2, 1, 3]
Postorder: [2, 3, 1]
2. Base Case Design
Every recursive tree function needs a base case for null nodes. The most common pattern:
if node is null:
return base_value (0, True, [], -infinity, etc.)
Choosing the right base value is critical. For height, return 0 (or -1 depending on convention). For "is valid BST," return True. For path sum, return False. The base value should be the identity element for your combining operation.
3. State Passing (Top-Down)
Pass information from parent to child through function parameters. This is the top-down approach — the parent tells its children what they need to know.
Examples:
- Validate BST: pass
(low, high)bounds down; each node checkslow < val < high - Path sum: pass the remaining target sum minus the current node's value
- Depth tracking: pass the current depth as a parameter, incrementing at each level
4. Bottom-Up Computation (Postorder)
Compute answers from leaves upward. Each node receives results from both children and combines them. This is the postorder approach — children report up to their parent.
Examples:
- Max depth:
1 + max(left_depth, right_depth) - Is balanced: check both subtrees are balanced AND their heights differ by at most 1
- Diameter: at each node, the longest path through it is
left_height + right_height
5. Iterative DFS with Explicit Stack
Replace recursion with your own stack. Push the root, then loop: pop a node, process it, push its children (right first, then left, so left is processed first).
This is useful when recursion depth might cause a stack overflow (very deep or skewed trees), or when you need fine-grained control over traversal order.
stack = [root]
while stack:
node = stack.pop()
process(node)
if node.right: stack.push(node.right)
if node.left: stack.push(node.left)
When DFS Fails
- You need level-by-level processing (e.g., "average of each level") -> use BFS (level-order traversal with a queue)
- You need the shortest path in an unweighted tree/graph -> BFS finds it naturally; DFS does not
- The tree is extremely wide and shallow -> BFS uses O(width) space, DFS uses O(height) which is better; but if you need level info, BFS is cleaner
- You need to process nodes in level order -> use BFS
Common Mistakes
- Forgetting the null base case: Every recursive function must handle
node == null. Missing this causes null pointer exceptions on leaf nodes' children. Always write the base case first before any other logic - Confusing top-down vs. bottom-up: Top-down passes state via parameters (preorder). Bottom-up returns results from children (postorder). Mixing them up leads to incorrect results. Ask yourself: "Does the parent need to tell the child something, or does the child need to report to the parent?"
- Mutating shared state incorrectly: When building paths or collecting results, passing a mutable list and forgetting to backtrack (remove the last element after recursing) causes paths to bleed into each other. Either copy the list at each call or explicitly backtrack
- Returning the wrong value from the base case: The base case return value must be the identity for your combining operation. Returning 0 for max depth is correct, but returning 0 for min depth is wrong (it would make every tree have min depth 0). Think carefully about what null means in your problem
- Stack overflow on skewed trees: A tree with n nodes can have height n (a linked list shape). Recursive DFS uses O(n) call stack space in this case. For very large trees, use iterative DFS with an explicit stack to avoid stack overflow
When to Use
Template
1# --- TreeNode Definition ---2class TreeNode:3def __init__(self, val=0, left=None, right=None):4self.val = val5self.left = left6self.right = right78# --- Recursive DFS Traversal Template ---9def dfs(root):10if not root:11return # base case1213# Preorder: process here (before children)14print(root.val)1516dfs(root.left)1718# Inorder: process here (between children)1920dfs(root.right)2122# Postorder: process here (after children)2324# --- Iterative DFS with Explicit Stack ---25def dfs_iterative(root):26if not root:27return []28result = []29stack = [root]30while stack:31node = stack.pop()32result.append(node.val)33# Push right first so left is processed first34if node.right:35stack.append(node.right)36if node.left:37stack.append(node.left)38return result
Syntax Reference
1# --- Python: TreeNode Definition ---2class TreeNode:3def __init__(self, val=0, left=None, right=None):4self.val = val5self.left = left6self.right = right78# --- Null Check (base case) ---9if not root:10return # base value1112# --- Access Children ---13root.left # left child (TreeNode or None)14root.right # right child (TreeNode or None)15root.val # node value1617# --- Recursive DFS Skeleton ---18def dfs(node):19if not node:20return base_value21left_result = dfs(node.left)22right_result = dfs(node.right)23return combine(node.val, left_result, right_result)2425# --- Create a Tree Node ---26node = TreeNode(5)27node.left = TreeNode(3)28node.right = TreeNode(7)
Common Recipes
1# --- Inorder Traversal (Iterative with Stack) ---2def inorder_traversal(root):3result = []4stack = []5curr = root6while curr or stack:7while curr:8stack.append(curr)9curr = curr.left10curr = stack.pop()11result.append(curr.val)12curr = curr.right13return result1415# --- Max Depth (Recursive) ---16def max_depth(root):17if not root:18return 019return 1 + max(max_depth(root.left), max_depth(root.right))2021# --- Invert Tree (Recursive) ---22def invert_tree(root):23if not root:24return None25root.left, root.right = invert_tree(root.right), invert_tree(root.left)26return root2728# --- Validate BST (with Bounds) ---29def is_valid_bst(root, lo=float('-inf'), hi=float('inf')):30if not root:31return True32if root.val <= lo or root.val >= hi:33return False34return (is_valid_bst(root.left, lo, root.val) and35is_valid_bst(root.right, root.val, hi))
Complexity
| Operation | Time | Space |
|---|---|---|
| DFS traversal (any order) | O(n) | O(h) |
| Find a node | O(n) | O(h) |
| Invert tree | O(n) | O(h) |
| Validate BST | O(n) | O(h) |
| Serialize / deserialize | O(n) | O(n) |
| Balanced tree: h = log n | - | O(log n) |
| Skewed tree: h = n | - | O(n) |
Watch Out
- ✗You need level-by-level processing (e.g., "average of each level") -> use BFS (level-order traversal with a queue)
- ✗You need the shortest path in an unweighted tree/graph -> BFS finds it naturally; DFS does not
- ✗The tree is extremely wide and shallow -> BFS uses O(width) space, DFS uses O(height) which is better; but if you need level info, BFS is cleaner
- ✗You need to process nodes in level order -> use BFS
- ✗Forgetting the null base case: Every recursive function must handle `node == null`. Missing this causes null pointer exceptions on leaf nodes' children. Always write the base case first before any other logic
- ✗Confusing top-down vs. bottom-up: Top-down passes state via parameters (preorder). Bottom-up returns results from children (postorder). Mixing them up leads to incorrect results. Ask yourself: "Does the parent need to tell the child something, or does the child need to report to the parent?"
- ✗Mutating shared state incorrectly: When building paths or collecting results, passing a mutable list and forgetting to backtrack (remove the last element after recursing) causes paths to bleed into each other. Either copy the list at each call or explicitly backtrack
- ✗Returning the wrong value from the base case: The base case return value must be the identity for your combining operation. Returning 0 for max depth is correct, but returning 0 for min depth is wrong (it would make every tree have min depth 0). Think carefully about what null means in your problem
- ✗Stack overflow on skewed trees: A tree with n nodes can have height n (a linked list shape). Recursive DFS uses O(n) call stack space in this case. For very large trees, use iterative DFS with an explicit stack to avoid stack overflow