title: Binary Tree Zigzag Level Order Traversal slug: binary-tree-zigzag-level-order-traversal difficulty: medium leetcode_id: 103 leetcode_url: https://leetcode.com/problems/binary-tree-zigzag-level-order-traversal/ categories: - trees - queue patterns: - slug: bfs is_optimal: true - slug: tree-traversal is_optimal: false function_signature: "def zigzag_level_order(root: TreeNode) -> list[list[int]]:" test_cases: visible: - input: { root: [3, 9, 20, null, null, 15, 7] } expected: [[3], [20, 9], [15, 7]] - input: { root: [1] } expected: [[1]] - input: { root: [] } expected: [] hidden: - input: { root: [1, 2, 3, 4, 5, 6, 7] } expected: [[1], [3, 2], [4, 5, 6, 7]] - input: { root: [1, 2, null, 3, null, 4] } expected: [[1], [2], [3], [4]] - input: { root: [1, 2, 3, 4, null, null, 5] } expected: [[1], [3, 2], [4, 5]] - input: { root: [0, -1, 100, -50, 50, -100, 200] } expected: [[0], [100, -1], [-50, 50, -100, 200]] - input: { root: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] } expected: [[1], [3, 2], [4, 5, 6, 7], [15, 14, 13, 12, 11, 10, 9, 8]] description: | Given the `root` of a binary tree, return *the zigzag level order traversal of its nodes' values* (i.e., from left to right, then right to left for the next level and alternate between). constraints: | - The number of nodes in the tree is in the range `[0, 2000]` - `-100 <= Node.val <= 100` examples: - input: "root = [3,9,20,null,null,15,7]" output: "[[3],[20,9],[15,7]]" explanation: "Level 0 (left to right): [3]. Level 1 (right to left): [20, 9]. Level 2 (left to right): [15, 7]." - input: "root = [1]" output: "[[1]]" explanation: "Single node tree returns one level with one element." - input: "root = []" output: "[]" explanation: "Empty tree returns an empty list." explanation: intuition: | Imagine reading a book where alternating pages are printed upside down. To read the content, you'd read one page normally (left to right), then flip the book to read the next page (effectively right to left), and keep alternating. This problem applies the same concept to tree traversal. A standard **level order traversal** (BFS) visits nodes level by level, always left to right. The "zigzag" twist requires us to **alternate the direction** at each level: left-to-right, then right-to-left, then left-to-right again, and so on. The core insight is that the underlying traversal is still BFS — we process nodes level by level using a queue. The only change is how we **arrange the values within each level**. We can either reverse every other level's values, or use a deque to build each level's list in the appropriate direction. Think of it like this: BFS gives us nodes in consistent left-to-right order. We just need a simple flag to track whether the current level should be reversed, and apply that transformation before adding to the result. approach: | We solve this using **BFS with level direction tracking**: **Step 1: Handle the empty tree edge case** - If `root` is `None`, return an empty list immediately   **Step 2: Initialise BFS structures** - `queue`: A deque starting with the root node for level-order traversal - `result`: An empty list to store the final zigzag traversal - `left_to_right`: A boolean flag set to `True`, indicating the direction for the current level   **Step 3: Process level by level** - While the queue is not empty: - Record the current level size (number of nodes at this level) - Create an empty list `level` to hold values for this level - Process all nodes at the current level: - Dequeue each node from the front - Append its value to `level` - Enqueue its left child (if exists), then right child (if exists) - If `left_to_right` is `False`, reverse the `level` list - Append `level` to `result` - Toggle `left_to_right` for the next level   **Step 4: Return the result** - Return `result` containing all levels in zigzag order   The key is that we always traverse children left-to-right in the queue, but we reverse the values of alternate levels before adding them to the result. This keeps the BFS logic clean and simple. common_pitfalls: - title: Modifying Queue Order Instead of Output description: | A tempting approach is to change the order of adding children to the queue based on direction — adding right child first for even levels, left child first for odd levels. This **breaks the traversal** for subsequent levels. If you add children in reverse order, the next level receives nodes in the wrong sequence, causing cascading errors. The correct approach keeps queue insertion order consistent (always left then right) and only reverses the output values for alternate levels. wrong_approach: "Alternate which child gets enqueued first" correct_approach: "Always enqueue left then right, reverse output on alternate levels" - title: Using a Stack Instead of Reversing description: | Some solutions try using a stack for zigzag levels. While this can work, it significantly complicates the code and is prone to off-by-one errors in tracking which structure to use. Reversing a level list in Python is O(k) where k is the level size, and since we visit each node exactly once overall, this doesn't change the O(n) time complexity. The simpler approach is almost always better. - title: Forgetting the Empty Tree Case description: | If `root` is `None`, attempting to add it to the queue and process it will cause errors. Always check for an empty tree at the start and return `[]` immediately. key_takeaways: - "**BFS for level-order**: Use a queue to process nodes level by level, tracking level boundaries with the queue's size" - "**Separate traversal from transformation**: Keep the core BFS logic clean; apply direction changes only to the output" - "**Flag-based alternation**: A simple boolean toggle handles the zigzag pattern elegantly" - "**Foundation for variants**: This pattern extends to spiral matrix traversal and other alternating-direction problems" time_complexity: "O(n). We visit each of the `n` nodes exactly once during traversal. Reversing levels takes O(n) total across all levels." space_complexity: "O(n). The queue holds at most one level of nodes at a time (up to `n/2` in a complete binary tree), and the result stores all `n` node values." solutions: - approach_name: BFS with Level Reversal is_optimal: true code: | from collections import deque def zigzag_level_order(root: TreeNode | None) -> list[list[int]]: # Handle empty tree if not root: return [] result = [] queue = deque([root]) left_to_right = True # First level goes left to right while queue: level_size = len(queue) level = [] # Process all nodes at current level for _ in range(level_size): node = queue.popleft() level.append(node.val) # Always add children left to right if node.left: queue.append(node.left) if node.right: queue.append(node.right) # Reverse if this level should go right to left if not left_to_right: level.reverse() result.append(level) left_to_right = not left_to_right # Toggle direction return result explanation: | **Time Complexity:** O(n) — Each node is visited once, and reversing all levels takes O(n) total. **Space Complexity:** O(n) — Queue holds at most one level (up to n/2 nodes), result stores all n values. This approach uses standard BFS with a direction flag. We always process children left-to-right in the queue, but reverse the values of alternate levels before adding to the result. This keeps the code simple and avoids the complexity of managing different queue orderings. - approach_name: BFS with Deque Insertion is_optimal: true code: | from collections import deque def zigzag_level_order(root: TreeNode | None) -> list[list[int]]: if not root: return [] result = [] queue = deque([root]) left_to_right = True while queue: level_size = len(queue) level = deque() # Use deque to build level in correct order for _ in range(level_size): node = queue.popleft() # Insert at appropriate end based on direction if left_to_right: level.append(node.val) # Add to right else: level.appendleft(node.val) # Add to left if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(list(level)) left_to_right = not left_to_right return result explanation: | **Time Complexity:** O(n) — Each node visited once, deque operations are O(1). **Space Complexity:** O(n) — Same as the reversal approach. Instead of reversing after collecting values, this approach builds each level's list in the correct order using a deque. For left-to-right levels, we append to the right; for right-to-left levels, we prepend to the left. Both approaches are equally optimal. - approach_name: Recursive DFS with Level Tracking is_optimal: false code: | def zigzag_level_order(root: TreeNode | None) -> list[list[int]]: result = [] def dfs(node: TreeNode | None, level: int) -> None: if not node: return # Extend result if we've reached a new level if level >= len(result): result.append([]) # Insert based on level parity if level % 2 == 0: result[level].append(node.val) # Left to right else: result[level].insert(0, node.val) # Right to left # Recurse on children dfs(node.left, level + 1) dfs(node.right, level + 1) dfs(root, 0) return result explanation: | **Time Complexity:** O(n^2) in worst case — `insert(0, val)` is O(k) for a list of size k, making odd levels costly. **Space Complexity:** O(n) for result plus O(h) recursion stack where h is tree height. This DFS approach tracks the current level and inserts values accordingly. While elegant, using `insert(0, val)` on a list is O(k), making this less efficient than BFS approaches for large trees. Included to show an alternative traversal strategy.