← DSA Patterns / Tree BFS / DFS

Binary Tree Level Order Traversal

The problem

Given the root of a binary tree, return its node values level by level, as a list of lists.

Example: for the tree below, the answer is [[3], [9, 20], [15, 7]].

      3
     / \
    9  20
       / \
      15  7

Depth-first traversals (pre/in/post-order) dive down a branch before visiting siblings — the wrong shape here. "Level by level" calls for breadth-first search: visit all nodes at distance d before any node at distance d+1. A queue does exactly that.

Approach 1 — DFS with a depth parameter

BFS is the natural fit, but DFS can also produce level groupings: pass the depth down the recursion and append each node to result[depth].

function levelOrder(root) {
  const result = []
  function dfs(node, depth) {
    if (!node) return
    if (result.length === depth) result.push([])
    result[depth].push(node.val)
    dfs(node.left, depth + 1)
    dfs(node.right, depth + 1)
  }
  dfs(root, 0)
  return result
}

Time: O(n). Space: O(h) recursion stack. Works because visiting left before right keeps each level list in left-to-right order. Good to know — but the queue version below is the one that generalizes to "zigzag", "right side view", and "rotting oranges"-style problems.

Approach 2 — BFS with a queue (the standard)

Push the root. Each loop iteration processes one full level: snapshot the current queue length, pop exactly that many nodes, and push their children (which form the next level).

function levelOrder(root) {
  if (!root) return []

  const result = []
  const queue = [root]

  while (queue.length > 0) {
    const levelSize = queue.length // snapshot BEFORE pushing children
    const level = []
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift()
      level.push(node.val)
      if (node.left) queue.push(node.left)
      if (node.right) queue.push(node.right)
    }
    result.push(level)
  }
  return result
}

Time: O(n) — each node enters and leaves the queue once. Space: O(w), the tree's maximum width (up to n/2 for a bushy tree).

The levelSize snapshot is the entire trick: without it, children pushed mid-level would bleed into the current level's loop.

Dry run

Tree: 3 with children 9, 20; 20 with children 15, 7.

IterationQueue at startlevelSizePoppedPushedresult
1[3]139, 20[[3]]
2[9, 20]29, then 2015, 7 (from 20)[[3], [9, 20]]
3[15, 7]215, then 7nothing[[3], [9, 20], [15, 7]]
4[]queue empty — done

Edge cases

  • Empty tree → [] (the early return).
  • Skewed tree (every node has one child) → each level is a single-element list; the queue never holds more than one node.
  • Array.prototype.shift() is O(n) in the worst case; for very large trees use an index pointer into the array instead of shifting. Interviewers rarely mind, but knowing it is a plus.
  • Variants that reuse this exact skeleton: Zigzag Level Order (alternate reversing level), Right Side View (keep the last node per level), Average of Levels.