Tree Traversal: Intuition Guide¶
The Mental Model¶
A binary tree is a specialized graph where each node has at most two children (left and right). Unlike general graphs, trees have:
- No cycles - You can never revisit a node by following edges
- Exactly one path between any two nodes
- Natural hierarchy - Parent/child relationships
The key insight: Trees decompose recursively. A tree is a root plus two smaller trees. This makes recursion the natural approach.
Two Fundamental Traversal Strategies¶
DFS (Depth-First Search)¶
Go as deep as possible before backtracking. Three flavors based on when you "process" the current node:
1
/ \
2 3
/ \
4 5
Preorder (Node β Left β Right): 1, 2, 4, 5, 3
Inorder (Left β Node β Right): 4, 2, 5, 1, 3
Postorder (Left β Right β Node): 4, 5, 2, 3, 1
When to use which order? - Preorder: Process node before children (e.g., copy/serialize tree) - Inorder: BST gives sorted order; useful for BST operations - Postorder: Process children before node (e.g., delete tree, compute subtree values)
BFS (Breadth-First Search)¶
Process level by level, using a queue:
When to use BFS? - Level-order output required - Finding nodes at specific depth - Zigzag or spiral traversal - Right-side view / boundary traversal
Pattern Recognition Signals¶
Signal: "Traverse and collect values"¶
Trigger phrases: "inorder traversal", "preorder", "postorder", "level order"
Mental model: Just run the appropriate traversal, collecting values.
# Inorder: Left β Node β Right
def inorder(node):
if not node: return
inorder(node.left)
result.append(node.val)
inorder(node.right)
Signal: "Compute tree property from subtrees"¶
Trigger phrases: "height", "depth", "number of nodes", "sum of values"
Mental model: Postorder thinking. Compute for children, combine at node.
Computing height:
1 (h=2)
/ \
(h=1) 2 3 (h=0)
/
(h=0) 4
height(node) = 1 + max(height(left), height(right))
Key insight: Most tree properties follow this pattern:
def compute(node):
if not node: return BASE_CASE
left_val = compute(node.left)
right_val = compute(node.right)
return COMBINE(node.val, left_val, right_val)
Signal: "Path through node" or "max path"¶
Trigger phrases: "longest path", "maximum path sum", "diameter"
Mental model: At each node, consider paths that use this node as the "apex" (highest point). Track global max while returning single-branch contribution.
Maximum path sum through node:
10 Best path through 10: 2 β 10 β 3 = 15
/ \ Left gain: max(0, 2) = 2
2 3 Right gain: max(0, 3) = 3
Return for parent: 10 + max(2, 3) = 13
Key insight: Return value β answer. You return what a parent can use (one branch), but the answer might include both branches.
Signal: "Check property holds for all nodes"¶
Trigger phrases: "is balanced", "is symmetric", "is valid BST"
Mental model: Early termination. Return False/sentinel as soon as property fails.
# Balanced tree check
def check(node):
if not node: return 0
left = check(node.left)
if left == -1: return -1 # Early terminate
right = check(node.right)
if right == -1: return -1 # Early terminate
if abs(left - right) > 1:
return -1 # Failed here
return 1 + max(left, right)
Key insight: Use sentinel values (-1) to propagate failure without extra state.
Signal: "Process by level" or "zigzag"¶
Trigger phrases: "level order", "right side view", "zigzag", "average of levels"
Mental model: BFS with level-size batching.
def level_order(root):
queue = deque([root])
while queue:
level_size = len(queue) # Snapshot current level size
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
result.append(level)
DFS vs BFS for Trees¶
| Use DFS when... | Use BFS when... |
|---|---|
| Computing recursive properties | Level-by-level processing needed |
| Path problems (root to leaf) | Finding nodes at specific depth |
| Backtracking on paths | Right-side view, boundary |
| Space: O(h) acceptable | Space: O(w) acceptable |
| Tree is balanced (h = log n) | Tree is deep/skewed (h = n) |
Space comparison for trees: - DFS: O(h) where h = height. Best case O(log n), worst case O(n) - BFS: O(w) where w = max width. Worst case O(n/2) = O(n) at bottom level
The "Return vs Update" Pattern¶
Many tree problems require tracking two things: 1. Return value: What a parent node needs 2. Global update: The actual answer
class Solution:
def diameterOfBinaryTree(self, root):
self.diameter = 0
def height(node):
if not node: return 0
left = height(node.left)
right = height(node.right)
# Update global answer (path through this node)
self.diameter = max(self.diameter, left + right)
# Return value for parent (single branch)
return 1 + max(left, right)
height(root)
return self.diameter
Problems using this pattern: - Diameter of Binary Tree (LC 543) - Binary Tree Maximum Path Sum (LC 124) - Longest Univalue Path (LC 687)
Common Pitfalls¶
Pitfall 1: Confusing height vs depth¶
Height: Distance from node DOWN to farthest leaf
Depth: Distance from root DOWN to node
1 (depth=0, height=2)
/ \
2 3 (depth=1, height=0)
/
4 (depth=2, height=0)
Pitfall 2: Forgetting base cases¶
# WRONG: Crashes on None
def height(node):
return 1 + max(height(node.left), height(node.right))
# CORRECT: Handle None
def height(node):
if not node: return 0 # or return -1 depending on definition
return 1 + max(height(node.left), height(node.right))
Pitfall 3: Not handling negative values in path sum¶
# WRONG: Always include subtree
def max_gain(node):
left = max_gain(node.left)
right = max_gain(node.right)
return node.val + left + right # Might decrease sum!
# CORRECT: Can skip negative subtrees
def max_gain(node):
left = max(0, max_gain(node.left)) # Skip if negative
right = max(0, max_gain(node.right)) # Skip if negative
return node.val + max(left, right)
Pitfall 4: Stack overflow on deep trees¶
# WRONG for deep trees (Python recursion limit ~1000)
def traverse(node):
if node.left: traverse(node.left)
if node.right: traverse(node.right)
# CORRECT: Iterative with explicit stack
def traverse_iterative(root):
stack = [root]
while stack:
node = stack.pop()
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
Practice Progression¶
Level 1: Basic Traversals (Master First!)¶
- LC 94 - Binary Tree Inorder Traversal: Core DFS pattern
- LC 102 - Binary Tree Level Order Traversal: Core BFS pattern
- LC 104 - Maximum Depth of Binary Tree: Basic recursion
Level 2: Property Computation¶
- LC 110 - Balanced Binary Tree: Early termination pattern
- LC 100 - Same Tree: Parallel recursion
- LC 101 - Symmetric Tree: Mirror comparison
Level 3: Path Problems¶
- LC 543 - Diameter of Binary Tree: Return vs update pattern
- LC 124 - Binary Tree Maximum Path Sum: Complex path tracking
- LC 112 - Path Sum: Root-to-leaf paths
Level 4: Advanced Applications¶
- LC 236 - LCA of Binary Tree: Ancestor finding
- LC 297 - Serialize/Deserialize Binary Tree: Tree encoding
- LC 105 - Construct from Preorder/Inorder: Tree building
Quick Decision Tree¶
Problem type...
βββ "Collect traversal values"
β βββ Specific order (inorder/pre/post) β DFS with correct order
β βββ Level by level β BFS
βββ "Compute property (height, sum, count)"
β βββ Single value β Postorder recursion
β βββ Check validity β Early termination with sentinel
βββ "Path problem"
β βββ Root to leaf β Track path during DFS
β βββ Any path (max sum, diameter) β Return + Update pattern
β βββ Path sum β DFS with target tracking
βββ "Level-based"
β βββ By-level output β BFS with size batching
β βββ Right-side view β BFS, take last of each level
β βββ Zigzag β BFS, alternate direction
βββ "Validate structure"
βββ Balanced β Height with -1 sentinel
βββ BST validity β Pass min/max bounds
βββ Symmetric β Two-pointer recursion
Related Patterns¶
| If you see... | Consider also... |
|---|---|
| Find path in tree | Graph DFS (if multiple paths) |
| Shortest path in tree | BFS (unweighted), but tree has unique path |
| Binary Search Tree | Binary Search pattern for O(log n) |
| Tree serialization | String parsing / State machines |
| Lowest Common Ancestor | Union-Find (for queries) |
| Tree to graph conversion | General graph algorithms |