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:
  - bfs
  - tree-traversal

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

    &nbsp;

    **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

    &nbsp;

    **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`

    &nbsp;

    **Step 4: Return the result**

    - Return `result` containing all levels from top to bottom

    &nbsp;

    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.
