feat(patterns): graph/tree traversal tutorials

This commit is contained in:
2025-08-18 22:00:08 +01:00
parent 83bf313305
commit a94e7f6142
4 changed files with 1128 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
name: BFS (Breadth-First Search)
slug: bfs
difficulty_level: 3
description: >
Level-by-level traversal using a queue, exploring all neighbors at the current
depth before moving deeper. This guarantees the shortest path in unweighted
graphs and is ideal for problems requiring layer-by-layer processing.
when_to_use: |
- Shortest path in unweighted graphs
- Level-order tree traversal
- Finding all nodes at distance K
- Word ladder / transformation problems
- Spreading simulations (rotting oranges, infection)
metaphor: |
Imagine dropping a stone into a still pond. Ripples spread outward in concentric
circles—first touching everything 1 meter away, then 2 meters, then 3 meters.
BFS works the same way: it explores all nodes at distance 1 before any node at
distance 2.
Another analogy: searching for someone in a building. You check everyone on your
current floor before taking the stairs to the next floor. You never go upstairs
until you've searched every room on the current level.
core_concept: |
BFS uses a **queue** (FIFO) to process nodes in the order they were discovered.
This guarantees that when you first reach a node, you've taken the shortest path
to get there (in terms of number of edges).
The key insight is that the queue naturally separates nodes by their distance
from the source. Everything added in round 1 is at distance 1. Everything added
in round 2 is at distance 2. By the time you dequeue a node, all closer nodes
have already been processed.
**Why it finds shortest paths**: If there were a shorter path to node X, you
would have discovered X earlier (through that shorter path) and already
processed it. The first time you reach X is always via the shortest route.
visualization: |
**Example: Shortest path from A to F**
```
Graph:
A --- B --- E
| |
C --- D --- F
BFS from A:
Queue: [A] Visited: {A} Level 0
Process A → add B, C
Queue: [B, C] Visited: {A,B,C} Level 1
Process B → add E, D
Process C → add D (skip, visited)
Queue: [E, D] Visited: {A,B,C,E,D} Level 2
Process E → no new neighbors
Process D → add F
Queue: [F] Visited: {A,B,C,E,D,F} Level 3
Process F → Found! Distance = 3
```
**Level-order tree traversal:**
```
1
/ \
2 3
/ \ \
4 5 6
Level 0: [1]
Level 1: [2, 3]
Level 2: [4, 5, 6]
Result: [[1], [2,3], [4,5,6]]
```
code_template: |
from collections import deque
def bfs_shortest_path(graph: dict, start: str, end: str) -> int:
"""Find shortest path distance in unweighted graph."""
if start == end:
return 0
queue = deque([start])
visited = {start}
distance = 0
while queue:
distance += 1
# Process all nodes at current level
for _ in range(len(queue)):
node = queue.popleft()
for neighbor in graph[node]:
if neighbor == end:
return distance
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return -1 # No path found
def bfs_level_order(root) -> list[list]:
"""Level-order traversal of binary tree."""
if not root:
return []
result = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
def bfs_matrix(grid: list[list[int]], start: tuple) -> int:
"""BFS on a 2D grid."""
rows, cols = len(grid), len(grid[0])
queue = deque([start])
visited = {start}
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
distance = 0
while queue:
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in directions:
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols
and (nr, nc) not in visited
and grid[nr][nc] != 0): # 0 = wall
visited.add((nr, nc))
queue.append((nr, nc))
distance += 1
return distance
recognition_signals:
- "shortest path"
- "minimum steps"
- "level order"
- "nearest"
- "unweighted graph"
- "distance"
- "spreading"
- "layer by layer"
- "all nodes at distance K"
- "word ladder"
- "rotting oranges"
common_mistakes:
- title: Adding to visited after dequeuing instead of when enqueuing
description: |
Marking nodes as visited when you dequeue them (instead of when you
enqueue them) causes the same node to be added multiple times, leading
to incorrect distances and wasted processing.
fix: |
Always mark nodes as visited immediately when adding to the queue:
```python
if neighbor not in visited:
visited.add(neighbor) # Mark NOW
queue.append(neighbor)
```
- title: Not tracking levels correctly
description: |
Processing the entire queue without tracking level boundaries makes it
impossible to know the distance or handle level-by-level logic.
fix: |
Use the "process current level size" pattern:
```python
for _ in range(len(queue)): # Fixed size at level start
node = queue.popleft()
# Process node
distance += 1 # Increment after level completes
```
- title: Using a list instead of deque
description: |
Using `list.pop(0)` is O(n) because all elements must shift. This turns
O(V+E) BFS into O(V²).
fix: |
Use `collections.deque` which has O(1) popleft:
```python
from collections import deque
queue = deque([start])
```
- title: Forgetting to check bounds in grid BFS
description: |
Moving to invalid coordinates (negative or beyond grid dimensions) causes
index errors.
fix: |
Always validate coordinates before accessing the grid:
```python
if 0 <= nr < rows and 0 <= nc < cols:
# Safe to access grid[nr][nc]
```
variations:
- name: Shortest path (unweighted)
description: |
Classic BFS to find minimum number of edges between two nodes.
example: "Word Ladder, Minimum Genetic Mutation, Open the Lock"
- name: Level-order traversal
description: |
Process tree nodes level by level, returning grouped results.
example: "Binary Tree Level Order Traversal, Zigzag Level Order"
- name: Multi-source BFS
description: |
Start BFS from multiple sources simultaneously. Useful for "spreading"
problems where multiple starting points expand in parallel.
example: "Rotting Oranges, Walls and Gates, 01 Matrix"
- name: Bidirectional BFS
description: |
Search from both start and end simultaneously, meeting in the middle.
Reduces search space from O(b^d) to O(b^(d/2)).
example: "Word Ladder (optimized), Shortest Path in large graphs"
- name: BFS with state
description: |
Track additional state beyond position (e.g., keys collected, walls broken).
State becomes part of the "node" being visited.
example: "Shortest Path in Grid with Obstacles Elimination"
related_patterns:
- dfs
- tree-traversal
- matrix-traversal
prerequisite_patterns: []

View File

@@ -0,0 +1,275 @@
name: DFS (Depth-First Search)
slug: dfs
difficulty_level: 3
description: >
Explore as deep as possible along each branch before backtracking, using
recursion or an explicit stack. DFS is memory-efficient and naturally suited
for problems involving paths, connectivity, and exhaustive exploration.
when_to_use: |
- Finding any path (not necessarily shortest)
- Detecting cycles in graphs
- Topological sorting
- Connected components
- Tree traversals (preorder, inorder, postorder)
- Exhaustive search when you need to explore all possibilities
metaphor: |
Imagine exploring a maze by always taking the first available turn until you
hit a dead end. Then you backtrack to the last intersection and try the next
option. You keep going deeper until stuck, then retreat and try alternatives.
Another analogy: reading a "choose your own adventure" book. You follow one
story path all the way to its ending, then go back and explore different
choices you didn't take.
core_concept: |
DFS uses a **stack** (LIFO)—either the call stack via recursion or an explicit
stack data structure. This naturally explores depth-first: the most recently
discovered node is explored first.
The key insight is that DFS maintains the current path from root to current
node on the stack. This makes it perfect for:
1. **Path problems**: The current path is always available during recursion
2. **Cycle detection**: If you encounter a node already on the current path,
there's a cycle
3. **Backtracking**: Naturally undo choices when returning from recursion
Unlike BFS, DFS doesn't guarantee shortest paths, but it uses O(h) memory
(height of tree/recursion depth) vs BFS's O(w) (width of level).
visualization: |
**Example: DFS traversal of graph**
```
Graph:
A --- B --- E
| |
C --- D --- F
DFS from A (alphabetical order):
Visit A → explore neighbors
Visit B → explore neighbors
Visit D → explore neighbors
Visit C (already visited? skip or detect cycle)
Visit F → no unvisited neighbors, backtrack
Backtrack to D, backtrack to B
Visit E → no unvisited neighbors, backtrack
Backtrack to B, backtrack to A
Visit C → D already visited, backtrack
Done
Order visited: A → B → D → F → E → C
```
**Recursion call stack visualization:**
```
dfs(A)
└─ dfs(B)
└─ dfs(D)
└─ dfs(F) ← returns
└─ dfs(C) ← already visited
└─ dfs(E) ← returns
└─ dfs(C) ← already visited
```
**Cycle detection with colors:**
```
WHITE (0) = unvisited
GRAY (1) = in current path (on stack)
BLACK (2) = fully processed
If we visit a GRAY node → cycle detected!
```
code_template: |
def dfs_recursive(graph: dict, start: str, visited: set = None) -> list:
"""Basic recursive DFS traversal."""
if visited is None:
visited = set()
visited.add(start)
result = [start]
for neighbor in graph[start]:
if neighbor not in visited:
result.extend(dfs_recursive(graph, neighbor, visited))
return result
def dfs_iterative(graph: dict, start: str) -> list:
"""Iterative DFS using explicit stack."""
visited = set()
stack = [start]
result = []
while stack:
node = stack.pop()
if node in visited:
continue
visited.add(node)
result.append(node)
# Add neighbors in reverse for same order as recursive
for neighbor in reversed(graph[node]):
if neighbor not in visited:
stack.append(neighbor)
return result
def dfs_path_finding(graph: dict, start: str, end: str) -> list:
"""Find a path from start to end using DFS."""
def dfs(node: str, path: list) -> list:
if node == end:
return path
for neighbor in graph[node]:
if neighbor not in path: # Avoid cycles
result = dfs(neighbor, path + [neighbor])
if result:
return result
return []
return dfs(start, [start])
def detect_cycle(graph: dict) -> bool:
"""Detect cycle in directed graph using DFS."""
WHITE, GRAY, BLACK = 0, 1, 2
color = {node: WHITE for node in graph}
def dfs(node: str) -> bool:
color[node] = GRAY # Mark as being processed
for neighbor in graph[node]:
if color[neighbor] == GRAY: # Back edge = cycle
return True
if color[neighbor] == WHITE and dfs(neighbor):
return True
color[node] = BLACK # Mark as complete
return False
return any(color[node] == WHITE and dfs(node) for node in graph)
def topological_sort(graph: dict) -> list:
"""Topological sort using DFS (Kahn's algorithm alternative)."""
visited = set()
result = []
def dfs(node: str):
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
dfs(neighbor)
result.append(node) # Add after all descendants
for node in graph:
if node not in visited:
dfs(node)
return result[::-1] # Reverse for topological order
recognition_signals:
- "find path"
- "detect cycle"
- "connected components"
- "topological sort"
- "all paths"
- "explore all"
- "tree traversal"
- "preorder"
- "postorder"
- "inorder"
- "number of islands"
- "flood fill"
common_mistakes:
- title: Stack overflow with deep recursion
description: |
Recursive DFS on large graphs or trees can exceed Python's default
recursion limit (usually 1000), causing a stack overflow.
fix: |
Either increase the limit with `sys.setrecursionlimit(10000)` or convert
to iterative DFS with an explicit stack. Iterative is safer for unknown
input sizes.
- title: Marking visited too late
description: |
In iterative DFS, checking visited only when popping (not when pushing)
allows the same node to be added multiple times, wasting memory.
fix: |
For iterative DFS, either check when popping (simpler but less efficient)
or mark visited when pushing (more efficient):
```python
if neighbor not in visited:
visited.add(neighbor) # Mark when adding
stack.append(neighbor)
```
- title: Confusing visited vs current path
description: |
For cycle detection in directed graphs, using a simple visited set doesn't
distinguish between "node in current path" (cycle) and "node fully processed"
(not a cycle, just already explored).
fix: |
Use three states (WHITE/GRAY/BLACK) or track the current path separately.
A cycle exists only if you revisit a node that's still on the current path.
- title: Wrong order in topological sort
description: |
Adding nodes to result before processing descendants gives reverse
topological order or incorrect order entirely.
fix: |
Add nodes to result only after all descendants are processed (post-order).
Then reverse the result at the end.
variations:
- name: Tree DFS (preorder/inorder/postorder)
description: |
Visit nodes in specific orders relative to processing children. Preorder
visits parent first, inorder visits between children, postorder visits
parent last.
example: "Binary Tree Inorder Traversal, Serialize/Deserialize BST"
- name: Graph DFS
description: |
Traverse all reachable nodes from a starting point. Need to track visited
nodes to avoid infinite loops in cyclic graphs.
example: "Number of Islands, Clone Graph, Course Schedule"
- name: DFS with backtracking
description: |
Explore paths and undo choices when they don't lead to a solution. The
natural call stack provides the backtracking mechanism.
example: "N-Queens, Sudoku Solver, Permutations"
- name: DFS with memoization
description: |
Cache results of DFS from each node to avoid recomputation. Transforms
DFS into dynamic programming for certain problems.
example: "Longest Increasing Path in Matrix, Word Break"
- name: Iterative deepening DFS
description: |
Run DFS with increasing depth limits. Combines BFS's shortest-path
guarantee with DFS's memory efficiency.
example: "Finding shortest path when memory is limited"
related_patterns:
- bfs
- backtracking
- tree-traversal
- matrix-traversal
prerequisite_patterns: []

View File

@@ -0,0 +1,299 @@
name: Matrix Traversal
slug: matrix-traversal
difficulty_level: 3
description: >
Navigate 2D grids systematically using DFS, BFS, or directional iteration.
Matrix traversal combines graph traversal concepts with coordinate-based
movement, treating each cell as a node connected to its neighbors.
when_to_use: |
- Finding connected regions (islands)
- Shortest path in a grid
- Flood fill / painting
- Spreading simulations (rotting oranges, fire spread)
- Word search in character grid
metaphor: |
Imagine being dropped into a maze viewed from above. You can move up, down,
left, or right, but walls block certain paths. DFS is like exploring one
corridor completely before backtracking. BFS is like sending scouts in all
directions simultaneously—whoever reaches the exit first found the shortest path.
Another analogy: painting a floor. Flood fill (DFS/BFS) starts at one point
and spreads to all connected unpainted tiles.
core_concept: |
A 2D grid is essentially a graph where each cell connects to its neighbors
(4-directional or 8-directional). The key adaptations from graph traversal:
1. **Nodes are coordinates**: Instead of node IDs, track `(row, col)` pairs
2. **Edges are directions**: Neighbors are found by adding direction vectors
3. **Bounds checking**: Validate coordinates before accessing the grid
4. **In-place visited marking**: Often modify the grid itself instead of using
a separate visited set (mark '1' → '0' to indicate visited)
**When to use DFS vs BFS:**
- **DFS**: Counting regions, checking if path exists, flood fill
- **BFS**: Shortest path, simultaneous spreading from multiple sources
visualization: |
**Example: Number of Islands**
```
Grid (1 = land, 0 = water):
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
DFS from (0,0): marks all connected 1s as visited
→ Found island 1
Continue scanning, DFS from (2,2):
→ Found island 2
Continue scanning, DFS from (3,3):
→ Found island 3
Answer: 3 islands
```
**Shortest path in grid (BFS):**
```
Grid (0 = passable, 1 = wall):
0 0 0
1 1 0
0 0 0
Start: (0,0) End: (2,2)
BFS levels:
Level 0: (0,0)
Level 1: (0,1)
Level 2: (0,2)
Level 3: (1,2)
Level 4: (2,2) ← Found! Distance = 4
```
**4-directional vs 8-directional:**
```
4-directional: 8-directional:
↑ ↖ ↑ ↗
← ○ → ← ○ →
↓ ↙ ↓ ↘
```
code_template: |
from collections import deque
# Direction vectors
DIRS_4 = [(0, 1), (0, -1), (1, 0), (-1, 0)] # right, left, down, up
DIRS_8 = [(0, 1), (0, -1), (1, 0), (-1, 0),
(1, 1), (1, -1), (-1, 1), (-1, -1)]
def is_valid(grid: list[list], row: int, col: int) -> bool:
"""Check if coordinates are within grid bounds."""
return 0 <= row < len(grid) and 0 <= col < len(grid[0])
def dfs_matrix(grid: list[list[int]], row: int, col: int) -> int:
"""DFS to explore connected region, returns size."""
if not is_valid(grid, row, col) or grid[row][col] == 0:
return 0
grid[row][col] = 0 # Mark as visited (modify in-place)
size = 1
for dr, dc in DIRS_4:
size += dfs_matrix(grid, row + dr, col + dc)
return size
def count_islands(grid: list[list[str]]) -> int:
"""Count connected regions of '1's."""
if not grid:
return 0
count = 0
rows, cols = len(grid), len(grid[0])
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs_sink_island(grid, r, c)
return count
def dfs_sink_island(grid: list[list[str]], row: int, col: int):
"""Sink an island by marking all connected '1's as '0'."""
if not is_valid(grid, row, col) or grid[row][col] == '0':
return
grid[row][col] = '0' # Sink this cell
for dr, dc in DIRS_4:
dfs_sink_island(grid, row + dr, col + dc)
def bfs_shortest_path(grid: list[list[int]],
start: tuple, end: tuple) -> int:
"""BFS to find shortest path in grid (0 = passable, 1 = wall)."""
if grid[start[0]][start[1]] == 1 or grid[end[0]][end[1]] == 1:
return -1
rows, cols = len(grid), len(grid[0])
queue = deque([(*start, 0)]) # (row, col, distance)
visited = {start}
while queue:
row, col, dist = queue.popleft()
if (row, col) == end:
return dist
for dr, dc in DIRS_4:
nr, nc = row + dr, col + dc
if (is_valid(grid, nr, nc)
and (nr, nc) not in visited
and grid[nr][nc] == 0):
visited.add((nr, nc))
queue.append((nr, nc, dist + 1))
return -1 # No path
def multi_source_bfs(grid: list[list[int]]) -> int:
"""BFS from multiple sources (e.g., rotting oranges)."""
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
# Find all sources and count targets
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2: # Rotten orange
queue.append((r, c, 0))
elif grid[r][c] == 1: # Fresh orange
fresh += 1
max_time = 0
while queue:
row, col, time = queue.popleft()
max_time = max(max_time, time)
for dr, dc in DIRS_4:
nr, nc = row + dr, col + dc
if is_valid(grid, nr, nc) and grid[nr][nc] == 1:
grid[nr][nc] = 2 # Mark as rotten
fresh -= 1
queue.append((nr, nc, time + 1))
return max_time if fresh == 0 else -1
recognition_signals:
- "grid"
- "matrix"
- "2D array"
- "number of islands"
- "shortest path in grid"
- "flood fill"
- "surrounded regions"
- "rotting oranges"
- "word search"
- "robot path"
- "maze"
common_mistakes:
- title: Modifying grid while using it for bounds checking
description: |
If you use grid values for validity checks (like `grid[r][c] == '1'`)
and also modify them in-place, you might check a cell you already modified.
fix: |
Modify the cell immediately upon visiting, before exploring neighbors:
```python
grid[row][col] = '0' # Mark first
for dr, dc in DIRS_4:
dfs(grid, row + dr, col + dc) # Then explore
```
- title: Forgetting to check all starting points
description: |
For problems like "number of islands," only starting DFS from (0,0) misses
islands not connected to the top-left.
fix: |
Iterate through every cell and start a new traversal from each unvisited cell:
```python
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(grid, r, c)
```
- title: Index out of bounds
description: |
Accessing `grid[row][col]` without checking bounds first causes runtime
errors when exploring neighbors.
fix: |
Always check bounds before accessing:
```python
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == target:
# Safe to access
```
- title: Using wrong direction set
description: |
Using 4-directional movement when problem requires 8-directional (diagonal)
gives wrong results, or vice versa.
fix: |
Read the problem carefully. "Adjacent" usually means 4-directional.
"Neighboring" or explicit diagonal mention means 8-directional.
variations:
- name: Island counting
description: |
Count connected regions of the same value. DFS from each unvisited cell,
marking the entire region as visited.
example: "Number of Islands, Max Area of Island"
- name: Shortest path in grid
description: |
BFS from start to end, counting levels. Works only if all moves have
equal cost (unweighted).
example: "Shortest Path in Binary Matrix, 01 Matrix"
- name: Multi-source BFS
description: |
Start BFS from multiple cells simultaneously. Useful for spreading
problems where multiple sources expand in parallel.
example: "Rotting Oranges, Walls and Gates"
- name: Word search
description: |
DFS with backtracking to find if a word exists in the grid by following
adjacent cells. Need to track the current path to avoid reusing cells.
example: "Word Search, Word Search II"
- name: Spiral traversal
description: |
Not graph-based, but iterate through matrix in spiral order using
boundary tracking (top, bottom, left, right).
example: "Spiral Matrix, Spiral Matrix II"
related_patterns:
- dfs
- bfs
- backtracking
prerequisite_patterns:
- dfs
- bfs

View File

@@ -0,0 +1,299 @@
name: Binary Tree Traversal
slug: tree-traversal
difficulty_level: 2
description: >
Visit all nodes in a binary tree in specific orders: preorder (root-left-right),
inorder (left-root-right), postorder (left-right-root), or level-order (BFS).
Each order reveals different structural information about the tree.
when_to_use: |
- Serializing/deserializing trees
- Validating BST properties (inorder gives sorted order)
- Computing tree properties (height, size, sum)
- Copying or comparing trees
- Path sum and path finding problems
metaphor: |
Imagine reading a family tree. **Preorder** is like announcing yourself first,
then introducing your children. **Inorder** is alphabetical—left child, then
you, then right child. **Postorder** is like calculating taxes—you need to
know your children's totals before computing your own.
Another way to think about it: **preorder** is top-down (decisions flow from
root to leaves), **postorder** is bottom-up (results bubble from leaves to root).
core_concept: |
The three traversal orders differ only in *when* you process the current node
relative to its children:
- **Preorder**: Process **before** children → good for copying trees, prefix expressions
- **Inorder**: Process **between** children → BST gives sorted order
- **Postorder**: Process **after** children → good for deletion, evaluating expressions
The key insight is that these traversals naturally map to different problems:
- Need to see parents before children? → **Preorder**
- Need to aggregate child results? → **Postorder**
- Need sorted BST elements? → **Inorder**
- Need level-by-level? → **BFS**
visualization: |
**Example tree:**
```
1
/ \
2 3
/ \
4 5
```
**Preorder (Root → Left → Right):**
```
Visit 1 → Visit 2 → Visit 4 → Visit 5 → Visit 3
Result: [1, 2, 4, 5, 3]
```
**Inorder (Left → Root → Right):**
```
Visit 4 → Visit 2 → Visit 5 → Visit 1 → Visit 3
Result: [4, 2, 5, 1, 3]
For BST: gives elements in sorted order!
```
**Postorder (Left → Right → Root):**
```
Visit 4 → Visit 5 → Visit 2 → Visit 3 → Visit 1
Result: [4, 5, 2, 3, 1]
Children processed before parent—useful for computing heights/sizes.
```
**Level-order (BFS):**
```
Level 0: [1]
Level 1: [2, 3]
Level 2: [4, 5]
Result: [1, 2, 3, 4, 5]
```
code_template: |
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def preorder_recursive(root: TreeNode) -> list:
"""Preorder: Root → Left → Right"""
if not root:
return []
return ([root.val]
+ preorder_recursive(root.left)
+ preorder_recursive(root.right))
def preorder_iterative(root: TreeNode) -> list:
"""Iterative preorder using stack."""
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# Push right first so left is processed first
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
def inorder_recursive(root: TreeNode) -> list:
"""Inorder: Left → Root → Right"""
if not root:
return []
return (inorder_recursive(root.left)
+ [root.val]
+ inorder_recursive(root.right))
def inorder_iterative(root: TreeNode) -> list:
"""Iterative inorder using stack."""
result = []
stack = []
current = root
while current or stack:
# Go left as far as possible
while current:
stack.append(current)
current = current.left
# Process current node
current = stack.pop()
result.append(current.val)
# Move to right subtree
current = current.right
return result
def postorder_recursive(root: TreeNode) -> list:
"""Postorder: Left → Right → Root"""
if not root:
return []
return (postorder_recursive(root.left)
+ postorder_recursive(root.right)
+ [root.val])
def postorder_iterative(root: TreeNode) -> list:
"""Iterative postorder using two stacks."""
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return result[::-1] # Reverse for postorder
def level_order(root: TreeNode) -> list[list]:
"""Level-order traversal using BFS."""
if not root:
return []
result = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
recognition_signals:
- "binary tree"
- "tree traversal"
- "preorder"
- "inorder"
- "postorder"
- "level order"
- "serialize tree"
- "flatten tree"
- "BST to sorted"
- "kth smallest in BST"
- "validate BST"
- "path sum"
common_mistakes:
- title: Stack order in iterative preorder
description: |
Pushing left child before right child processes right first because
stacks are LIFO.
fix: |
Push right child first, then left child:
```python
if node.right:
stack.append(node.right) # Push right first
if node.left:
stack.append(node.left) # Left popped first
```
- title: Iterative inorder is tricky
description: |
The iterative inorder traversal is harder to get right than preorder or
postorder because you need to track when to go left vs when to process.
fix: |
Use the "go left until null, then process and go right" pattern:
```python
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
process(current)
current = current.right
```
- title: Forgetting null checks
description: |
Accessing `node.left` or `node.right` without checking if node is None
causes attribute errors.
fix: |
Always check for null nodes first:
```python
if not root:
return []
```
- title: Modifying tree during traversal
description: |
Changing node values or structure while traversing can cause missed nodes
or infinite loops.
fix: |
Collect nodes to modify in a separate pass, or use a traversal that
processes children before modifying the parent.
variations:
- name: Morris Traversal
description: |
Inorder traversal using O(1) space by temporarily modifying the tree
(threading right pointers to inorder successors).
example: "Recover Binary Search Tree, Inorder without stack"
- name: Boundary Traversal
description: |
Traverse only the boundary of the tree: left boundary, leaves, right
boundary in reverse.
example: "Boundary of Binary Tree"
- name: Vertical Order Traversal
description: |
Group nodes by their horizontal distance from root. Nodes at same
horizontal distance are in the same vertical line.
example: "Vertical Order Traversal, Binary Tree Right Side View"
- name: Zigzag Level Order
description: |
Level-order but alternating direction: left-to-right, then right-to-left,
and so on.
example: "Binary Tree Zigzag Level Order Traversal"
- name: Tree Serialization
description: |
Use traversal order to convert tree to string and back. Preorder with
null markers is common.
example: "Serialize and Deserialize Binary Tree"
related_patterns:
- dfs
- bfs
prerequisite_patterns: []