HARD Lab · Trees · 66

102. Binary Tree Level Order Traversal

Return the node values level by level, top to bottom. A breadth-first sweep with a queue does it — the trick is to snapshot the queue size at the start of each level so you know exactly where one row ends and the next begins.

MediumBFSQueueTypeScript

PROBLEM What we're solving

Given a binary tree, return its node values grouped by depth: one inner array per level, ordered top-to-bottom and left-to-right within each level. For the tree [3,9,20,null,null,15,7] (root 3; children 9 and 20; 20's children 15 and 7), the answer is [[3], [9,20], [15,7]]. An empty tree returns [].

KEY IDEA BFS, but batch one level per outer step

Insight → a plain queue BFS visits nodes in level order automatically, but it flattens them into one stream. To recover the level boundaries, record levelSize = queue.length before you start dequeuing. Those levelSize nodes are exactly the current level; every child you enqueue while draining them belongs to the next level. Loop exactly levelSize times and you carve the stream into rows.

RECIPE Seed, snapshot, drain, record

  • 0 · Guard & seed. If the root is null return []. Otherwise put the root in a queue.
  • 1 · Snapshot the width. At the top of each outer iteration set levelSize = queue.length. This freezes how many nodes are on the current level before any children are added.
  • 2 · Drain exactly that many. Loop levelSize times: dequeue a node, push its value into the current level's array, and enqueue its non-null children for later — why: the children form the next level, which we'll process on the following outer iteration.
  • 3 · Record the row. Push the finished level array onto the result. The queue now holds precisely the next level.
  • 4 · Repeat until the queue empties.
Classic confusion → don't write for (k = 0; k < queue.length; k++) with the live queue.length. You mutate the queue inside the loop, so the bound keeps growing and you swallow the next level into the current one. Capture levelSize into a separate variable first.

COST Complexity & alternatives

Re-scan tree per level (depth queries)
O(n · h)
Recomputing each level independently re-walks ancestors.
Single BFS with size snapshot
O(n)
Each node enqueued and dequeued once.

Space note

The queue holds at most one full level at a time, so space is O(w) where w is the maximum width — up to n/2 for the bottom row of a full tree, i.e. O(n) worst case. The output itself is O(n).

Pattern transfer → the size-snapshot trick is the backbone of nearly every level-aware BFS: Zigzag Level Order (reverse alternate rows), Right Side View (take the last node of each level), Average of Levels, Minimum Depth (first level with a leaf), and Rotting Oranges (BFS by minute).

RUN IT Snapshot the queue size, then drain one level at a time

step 0 / 17
STARTSeed the queue with the root 3. We process the tree breadth-first, one level per outer iteration.
queue3
State
0
level #
[]
result
node being dequeuedin queue / current levellevel counteraccumulated result
slowfast

TYPESCRIPT The solution, annotated

levelOrder.ts
class TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
    this.val = val;
    this.left = left;
    this.right = right;
  }
}

function levelOrder(root: TreeNode | null): number[][] {
  const result: number[][] = [];
  if (root === null) return result;       // empty tree -> []

  const queue: TreeNode[] = [root];       // seed with the root
  while (queue.length > 0) {
    const levelSize = queue.length;       // SNAPSHOT this level's width
    const level: number[] = [];

    for (let k = 0; k < levelSize; k++) { // drain exactly levelSize nodes
      const node = queue.shift()!;        // dequeue from the front
      level.push(node.val);
      if (node.left) queue.push(node.left);   // children -> next level
      if (node.right) queue.push(node.right);
    }
    result.push(level);                   // one array per depth
  }
  return result;
}

Reading it block by block

Guard. An empty tree has no levels, so return an empty result immediately. This also keeps the loop logic clean — we never seed a null root.
Seed the queue. Start BFS with just the root. The while loop runs once per level of the tree.
The snapshot. const levelSize = queue.lengthcaptures the current level's node count before we touch the queue. This single line is what turns flat BFS into a level-grouped traversal.
Drain the level. The inner for loop dequeues exactly levelSize nodes. Each contributes its value to level and hands its children to the queue for the next round. Because the bound is the frozen levelSize, the children we just pushed are not consumed here.
Record & repeat. Push the completed level array onto result. The queue now contains exactly the next level, so the loop continues until it drains.
Complexity → O(n) time — each node is enqueued and dequeued exactly once. O(n) space worst case for the queue (the widest level can hold ~n/2 nodes), plus O(n) for the output.

INTERVIEWFollow-ups they'll ask

  • "Return the levels bottom-up?" (LC 107) Build the same way, then result.reverse() — or unshift each level.
  • "Zigzag order?" (LC 103) Track a boolean that flips each level and reverse the level array (or build it left/right) on odd levels.
  • "Right-side view?" (LC 199) Same loop, but only keep the last node dequeued in each level.
  • "Can you do it with recursion / DFS?" Yes — pass a depth parameter and append to result[depth], creating the row on first visit. Same O(n), O(h) stack.
  • "Why not queue.shift() performance?" Array shift is O(n); for huge trees use a head index pointer or a deque to keep dequeue O(1).

MNEMONIC The one-liner

"Seed the root, snapshot the size, drain that many, record the row."

TRIGGERS When you see ___ → reach for ___

"return values level by level"BFS with a queue
need per-level groupingsnapshot levelSize = queue.length
"right view / zigzag / level average"same loop, tweak what you keep
shortest path in unweighted grid/treeBFS counts levels = distance

SKELETON The reusable shape

skeleton.ts
const result: number[][] = [];
if (!root) return result;
const queue: TreeNode[] = [root];
while (queue.length) {
  const levelSize = queue.length;   // snapshot!
  const level: number[] = [];
  for (let k = 0; k < levelSize; k++) {
    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;

FLASHCARDS Tap to flip

What single line groups a flat BFS into levels?
const levelSize = queue.length taken at the top of each outer iteration, before any children are enqueued.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the purpose of saving levelSize = queue.length before the inner loop?
QUESTION 02
levelOrder([3,9,20,null,null,15,7]) returns:
QUESTION 03
Time complexity of the BFS level-order solution?
QUESTION 04
If you wrote for (let k = 0; k < queue.length; k++) using the live length, what happens?
QUESTION 05
Worst-case space used by the queue?
QUESTION 06
Which data structure makes the traversal breadth-first?
QUESTION 07
To produce a right-side view (LC 199) with this same loop, you would: