title: Binary Tree Level Order Traversal slug: binary-tree-level-order-traversal difficulty: medium leetcode_id: 102 leetcode_url: https://leetcode.com/problems/binary-tree-level-order-traversal/ categories: - trees - queue patterns: - slug: bfs is_optimal: true - slug: tree-traversal is_optimal: false function_signature: "def level_order(root: TreeNode | None) -> list[list[int]]:" test_cases: visible: - input: { root: [3, 9, 20, null, null, 15, 7] } expected: [[3], [9, 20], [15, 7]] - input: { root: [1] } expected: [[1]] - input: { root: [] } expected: [] hidden: - input: { root: [1, 2, 3, 4, 5] } expected: [[1], [2, 3], [4, 5]] - input: { root: [1, 2, null, 3] } expected: [[1], [2], [3]] - input: { root: [1, null, 2, null, 3] } expected: [[1], [2], [3]] description: | Given the `root` of a binary tree, return *the level order traversal of its nodes' values*. (i.e., from left to right, level by level). constraints: | - The number of nodes in the tree is in the range `[0, 2000]` - `-1000 <= Node.val <= 1000` examples: - input: "root = [3,9,20,null,null,15,7]" output: "[[3],[9,20],[15,7]]" explanation: "The tree has 3 levels: root node 3 at level 0, nodes 9 and 20 at level 1, and nodes 15 and 7 at level 2." - input: "root = [1]" output: "[[1]]" explanation: "A single node tree has only one level containing that node." - input: "root = []" output: "[]" explanation: "An empty tree has no levels to traverse." explanation: intuition: | Imagine you're taking a group photo of a family tree — literally. You want everyone on the same generation (level) to stand in the same row, from left to right. You'd naturally process the photo row by row, starting with the oldest generation at the top. This is exactly what **level order traversal** does: visit all nodes at depth 0, then all nodes at depth 1, then depth 2, and so on. The key insight is that this is a **Breadth-First Search (BFS)** problem — we explore all neighbors (children) at the current level before moving deeper. Think of it like ripples spreading outward from a stone dropped in water. The ripples expand level by level, touching all points at distance 1 before reaching distance 2. A **queue** is the perfect data structure for this because it processes nodes in First-In-First-Out (FIFO) order — exactly what we need to respect the level-by-level ordering. The trick is knowing when one level ends and the next begins. We do this by processing all nodes currently in the queue (one level's worth) before moving to the next batch. approach: | We solve this using **Breadth-First Search (BFS)** with a queue: **Step 1: Handle the empty tree case** - If `root` is `None`, return an empty list `[]` - This avoids errors when trying to add `None` to the queue   **Step 2: Initialise the data structures** - `result`: An empty list to store each level's values - `queue`: A deque (double-ended queue) initialised with the root node   **Step 3: Process the tree level by level** - While the queue is not empty: - Determine the number of nodes at the current level: `level_size = len(queue)` - Create an empty list `current_level` to store this level's values - Process exactly `level_size` nodes (this is the key to separating levels): - Dequeue a node from the front - Add its value to `current_level` - Enqueue its left child (if it exists) - Enqueue its right child (if it exists) - After processing all nodes at this level, append `current_level` to `result`   **Step 4: Return the result** - Return `result` containing all levels from top to bottom   The queue ensures we process nodes in the correct left-to-right order, and capturing `level_size` at the start of each iteration guarantees we process exactly one level before moving to the next. common_pitfalls: - title: Not Separating Levels Correctly description: | A common mistake is to process nodes one at a time without tracking where one level ends and the next begins. This results in a flat list of values rather than grouped levels. For example, processing `[3,9,20,null,null,15,7]` incorrectly might give `[3, 9, 20, 15, 7]` instead of `[[3], [9, 20], [15, 7]]`. The fix is to capture `level_size = len(queue)` at the start of each outer loop iteration and process exactly that many nodes before moving to the next level. wrong_approach: "Process nodes one at a time without level boundaries" correct_approach: "Capture level_size at start of each iteration" - title: Forgetting to Check for Null Children description: | When enqueueing children, you must check if they exist. Adding `None` to the queue will cause errors when you try to access `.val`, `.left`, or `.right` on a null node. Always use `if node.left:` and `if node.right:` guards before enqueueing. wrong_approach: "queue.append(node.left) without checking" correct_approach: "if node.left: queue.append(node.left)" - title: Using a List Instead of a Deque description: | While you can use a regular Python list as a queue with `append()` and `pop(0)`, this is inefficient. `pop(0)` is O(n) because it shifts all remaining elements. Using `collections.deque` with `append()` and `popleft()` gives O(1) operations for both enqueue and dequeue, which matters for larger trees. wrong_approach: "list.pop(0) for dequeue — O(n) per operation" correct_approach: "deque.popleft() — O(1) per operation" - title: Forgetting the Empty Tree Edge Case description: | If `root` is `None`, the function should return `[]`. If you initialise the queue with `root` without checking, you'll add `None` to the queue and crash when accessing its attributes. Always handle the empty tree case first. key_takeaways: - "**BFS is the natural fit** for level-by-level traversal — the queue's FIFO ordering preserves the left-to-right, top-to-bottom order" - "**Level separation trick**: Capture `len(queue)` at the start of each iteration to know exactly how many nodes belong to the current level" - "**Deque for efficiency**: Use `collections.deque` for O(1) enqueue and dequeue operations" - "**Foundation for related problems**: This pattern extends to zigzag traversal, right side view, average of levels, and many other tree problems" time_complexity: "O(n). We visit each node exactly once, where n is the number of nodes in the tree." space_complexity: "O(n). In the worst case (a complete binary tree), the last level contains up to n/2 nodes, all of which would be in the queue simultaneously." solutions: - approach_name: BFS with Queue is_optimal: true code: | 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 level_order(root: TreeNode | None) -> list[list[int]]: # Handle empty tree if not root: return [] result = [] queue = deque([root]) # Start with the root node while queue: # Number of nodes at current level level_size = len(queue) current_level = [] # Process all nodes at this level for _ in range(level_size): node = queue.popleft() current_level.append(node.val) # Add children for next level (left to right order) if node.left: queue.append(node.left) if node.right: queue.append(node.right) # Add this level to result result.append(current_level) return result explanation: | **Time Complexity:** O(n) — Each node is visited exactly once. **Space Complexity:** O(n) — The queue holds at most one level of nodes. For a complete binary tree, the last level has approximately n/2 nodes. This BFS approach processes nodes level by level. The key insight is using `level_size = len(queue)` to know exactly how many nodes belong to the current level before we start adding their children for the next level. - approach_name: DFS with Level Tracking is_optimal: false code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def level_order(root: TreeNode | None) -> list[list[int]]: result = [] def dfs(node: TreeNode | None, level: int) -> None: if not node: return # Create new level list if needed if level == len(result): result.append([]) # Add node to its level result[level].append(node.val) # Recurse on children with incremented level dfs(node.left, level + 1) dfs(node.right, level + 1) dfs(root, 0) return result explanation: | **Time Complexity:** O(n) — Each node is visited exactly once. **Space Complexity:** O(h) for recursion stack where h is the tree height, plus O(n) for the result. In the worst case (skewed tree), h = n. This DFS approach tracks the current level as a parameter. When visiting a node, we add it to the list for its level. By visiting left before right, we maintain left-to-right order within each level. While this also works, BFS is more intuitive for level-order problems and doesn't risk stack overflow on very deep trees.