Problem
Given the root of a binary tree, return the values of its nodes grouped by depth level, from the root down to the leaves. Each depth gets its own inner list in the output.
Example:
[3,9,20,null,null,15,7]
- Input:
root = [3, 9, 20, null, null, 15, 7] - Output:
[[3], [9, 20], [15, 7]] - Explanation: Level 0 holds just the root (3), level 1 holds its two children (9, 20), and level 2 holds their children (15, 7).
Intuition
The problem is a breadth-first traversal: finish all nodes at depth 0 before moving to depth 1, then depth 2, and so on. A queue enforces this ordering naturally โ when you dequeue a node, you enqueue its children, guaranteeing each generation is fully processed before the next one begins. The key insight is snapshotting the queue's size at the start of each iteration; that count tells you exactly how many nodes belong to the current level before any new children are added.
Solution โ BFS with Level Snapshot
Initialize a queue with the root and process it level by level. At each step, capture the current queue length before enqueueing any children โ that snapshot count bounds the inner loop to exactly the nodes at the current depth.
Steps:
- Return an empty list immediately if root is null.
- Initialize a queue containing only the root.
- While the queue is non-empty, record
level_size = len(queue). - Dequeue exactly
level_sizenodes, appending each value to a temporary list. - For each dequeued node, enqueue its non-null left and right children.
- Append the temporary list as one completed level in the result.
- Return result once the queue is empty.
1from collections import deque
2
3def levelOrder(root):
4 if not root:
5 return []
6 result = []
7 queue = deque([root])
8 while queue:
9 level_size = len(queue) # snapshot before any children are enqueued this round
10 level_values = []
11 for _ in range(level_size):
12 node = queue.popleft()
13 level_values.append(node.val)
14 if node.left:
15 queue.append(node.left)
16 if node.right:
17 queue.append(node.right)
18 result.append(level_values)
19 return result- Time: O(n) โ every node is enqueued and dequeued exactly once.
- Space: O(n) โ the queue holds at most the widest level; a complete binary tree's bottom row contains roughly n/2 nodes.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| BFS with level snapshot | O(n) | O(n) | Any time you need nodes grouped by depth |
Common Mistakes
- Not snapshotting the queue size before the inner loop โ using
while queueas the inner loop condition drains the entire queue in a single pass, collapsing all levels into one flat list. Always capturelevel_size = len(queue)before enqueueing any children. - Enqueueing null children โ failing to check
if node.leftbefore appending causes a null pointer crash when the next iteration dequeues that null and tries to read.val; guard both children explicitly. - Returning
[[]]instead of[]for an empty tree โ skipping the null check on root causes the outer while loop to run once on an empty queue, appending an empty level to the result. Return[]immediately when root is null. - Using a null sentinel to separate levels โ enqueueing
nullafter each level's nodes is a valid alternative, but it silently breaks if root itself is null and requires extra handling at initialization. The snapshot approach is simpler and avoids sentinel logic entirely.
Related Problems
binary-tree-zigzag-level-order-traversalโ same BFS skeleton but alternates the direction each level is appendedbinary-tree-right-side-viewโ level-order traversal where you only keep each level's last noderotting-orangesโ multi-source BFS on a grid using the same level-snapshot patternmaximum-level-sum-of-a-binary-treeโ level-order traversal with per-level aggregation to find the maximum summinimum-depth-of-binary-treeโ BFS naturally stops at the first level containing a leaf, making it more efficient than DFS here