Trees (BFS)
Traverse binary trees level by level using a queue. Master the queue-plus-level-size pattern to solve problems that require processing nodes one level at a time, including level-order traversal, per-level aggregation, and BFS-based serialization.
Learn & Reference
Understanding the Pattern
Trees (BFS) Pattern
Breadth-first search (BFS) explores a binary tree level by level, visiting all nodes at depth 0, then all at depth 1, then depth 2, and so on. While DFS dives deep along one branch before backtracking, BFS sweeps horizontally across the tree. The core data structure is a queue — nodes enter at the back and leave from the front, naturally producing level-order output.
Core Concept
BFS starts by enqueueing the root. On each iteration, it dequeues a node, processes it, and enqueues that node's children. Because children are always added after their parent's level, the queue always contains nodes in level order.
1 Level 0: [1]
/ \
2 3 Level 1: [2, 3]
/ \ \
4 5 6 Level 2: [4, 5, 6]
BFS visit order: 1, 2, 3, 4, 5, 6
The key challenge in interviews is not just visiting nodes in order — it is knowing which nodes belong to the same level. This is where the level-size trick comes in.
Key Techniques
1. The Level-Size Trick
This is THE BFS interview pattern. At the start of each level, snapshot the current queue length. That number tells you exactly how many nodes belong to the current level. Process that many nodes, enqueueing their children, and you have cleanly separated one level from the next.
while queue is not empty:
level_size = len(queue) # snapshot!
for i in range(level_size):
node = queue.popleft()
process(node)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
Without the snapshot, newly enqueued children mix with the current level's nodes, and you lose track of level boundaries.
2. Per-Level Aggregation
Once you can isolate each level, you can extract information per level:
- Collect all values: level-order traversal — append each level's values as a subarray
- Last element only: right-side view — the last node processed at each level is the rightmost
- First element only: left-side view — the first node at each level
- Max / min / average: compute aggregates over the level's nodes
The template is always the same: level-size snapshot, inner loop, extract what you need.
3. Direction Alternation (Zigzag)
Some problems require alternating the direction each level is read. Level 0 left-to-right, level 1 right-to-left, level 2 left-to-right, and so on.
The simplest approach: collect each level normally (left-to-right via BFS), then reverse the level array when the direction flag says right-to-left. Toggle the flag after each level.
4. BFS-Based Serialization
BFS can serialize a tree by recording node values level by level, using a sentinel (like "N") for null children. Deserialization reads tokens in the same level-order: create the root from the first token, then for each parent node in a queue, read two tokens for its left and right children.
This is an alternative to DFS preorder serialization. BFS serialization produces a format that matches the intuitive "array representation" of a tree.
5. BFS vs DFS — When to Choose BFS
Use BFS when:
- You need level-by-level processing (e.g., level-order traversal, averages per level)
- You need the shallowest occurrence (shortest path in an unweighted tree)
- You need per-level metadata (level number, width, rightmost/leftmost node)
Use DFS when:
- The problem is naturally recursive (max depth, validate BST, path sum)
- You need root-to-leaf paths
- Space matters and the tree is balanced (DFS uses O(h) vs BFS O(w))
- The tree is narrow and deep — DFS stays within O(h) while BFS may hold many nodes
When BFS Fails
- Deep recursive properties: computing subtree sums, validating BST constraints, or finding lowest common ancestor are far more natural with DFS
- Very wide trees: a complete binary tree at depth d has 2^d nodes in the last level — BFS holds all of them in the queue at once
- Path-from-root problems: DFS naturally tracks the path via the call stack; BFS would need an auxiliary parent map
Common Mistakes
- Forgetting to snapshot queue length: Without
level_size = len(queue)at the start of each level, you process children from the next level within the current level's loop, corrupting your level boundaries. Always capture the queue length before the inner loop - Modifying the queue while iterating: In languages where
for node in queueiterates over a live collection, appending children during iteration causes infinite loops or skipped nodes. Always use an index-based loop with the snapshot length - Not handling the empty tree: When root is null, you must return early before initializing the queue. Enqueueing null and then trying to access
.valcauses a null pointer error - Using BFS when DFS is simpler: For problems like max depth or validate BST, BFS works but requires more code than a 3-line recursive DFS. Choose the simpler tool for the problem
- Forgetting to initialize level metadata: Many BFS problems need a flag (zigzag direction), a counter (level number), or an accumulator (level sum). Failing to reset or toggle these between levels leads to subtle bugs
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# --- BFS Level-Order Template ---9from collections import deque1011def bfs(root):12if not root:13return []14result = []15queue = deque([root])16while queue:17level_size = len(queue) # snapshot current level18level = []19for _ in range(level_size):20node = queue.popleft()21level.append(node.val) # process node22if node.left:23queue.append(node.left)24if node.right:25queue.append(node.right)26result.append(level) # finished one level27return result
Syntax Reference
1# --- Python: Queue for BFS ---2from collections import deque34# Create a queue and seed with root5queue = deque([root])67# Dequeue from front8node = queue.popleft() # O(1)910# Enqueue to back11queue.append(child) # O(1)1213# Check if empty14if not queue:15# queue is empty1617# Queue length (for level-size trick)18level_size = len(queue)1920# --- TreeNode Definition ---21class TreeNode:22def __init__(self, val=0, left=None, right=None):23self.val = val24self.left = left25self.right = right
Common Recipes
1# --- Level-Order Traversal (with level grouping) ---2from collections import deque34def level_order(root):5if not root:6return []7result = []8queue = deque([root])9while queue:10level_size = len(queue)11level = []12for _ in range(level_size):13node = queue.popleft()14level.append(node.val)15if node.left:16queue.append(node.left)17if node.right:18queue.append(node.right)19result.append(level)20return result2122# --- Right Side View (BFS) ---23def right_side_view(root):24if not root:25return []26result = []27queue = deque([root])28while queue:29level_size = len(queue)30for i in range(level_size):31node = queue.popleft()32if i == level_size - 1:33result.append(node.val)34if node.left:35queue.append(node.left)36if node.right:37queue.append(node.right)38return result3940# --- Zigzag Level Order ---41def zigzag_level_order(root):42if not root:43return []44result = []45queue = deque([root])46left_to_right = True47while queue:48level_size = len(queue)49level = deque()50for _ in range(level_size):51node = queue.popleft()52if left_to_right:53level.append(node.val)54else:55level.appendleft(node.val)56if node.left:57queue.append(node.left)58if node.right:59queue.append(node.right)60result.append(list(level))61left_to_right = not left_to_right62return result
Complexity
| Operation | Time | Space |
|---|---|---|
| BFS traversal (all nodes) | O(n) | O(w) |
| Level-order with grouping | O(n) | O(w) |
| Right/left side view | O(n) | O(w) |
| Zigzag level order | O(n) | O(w) |
| BFS serialize/deserialize | O(n) | O(n) |
| Balanced tree: w ≈ n/2 | - | O(n) |
| Skewed tree: w = 1 | - | O(1) |
Watch Out
- ✗**Deep recursive properties**: computing subtree sums, validating BST constraints, or finding lowest common ancestor are far more natural with DFS
- ✗**Very wide trees**: a complete binary tree at depth d has 2^d nodes in the last level — BFS holds all of them in the queue at once
- ✗**Path-from-root problems**: DFS naturally tracks the path via the call stack; BFS would need an auxiliary parent map
- ✗Forgetting to snapshot queue length: Without `level_size = len(queue)` at the start of each level, you process children from the next level within the current level's loop, corrupting your level boundaries. Always capture the queue length before the inner loop
- ✗Modifying the queue while iterating: In languages where `for node in queue` iterates over a live collection, appending children during iteration causes infinite loops or skipped nodes. Always use an index-based loop with the snapshot length
- ✗Not handling the empty tree: When root is null, you must return early before initializing the queue. Enqueueing null and then trying to access `.val` causes a null pointer error
- ✗Using BFS when DFS is simpler: For problems like max depth or validate BST, BFS works but requires more code than a 3-line recursive DFS. Choose the simpler tool for the problem
- ✗Forgetting to initialize level metadata: Many BFS problems need a flag (zigzag direction), a counter (level number), or an accumulator (level sum). Failing to reset or toggle these between levels leads to subtle bugs