Breadth‑First Search: Solving Binary Tree Level Order Traversal

Hero image for Breadth‑First Search: Solving Binary Tree Level Order Traversal. Image by Wesley Tingey.
Hero image for 'Breadth‑First Search: Solving Binary Tree Level Order Traversal.' Image by Wesley Tingey.

Binary Tree Level Order Traversal is one of those problems where the right pattern shows itself almost immediately once we stop forcing recursion onto everything. We are asked to return the tree level by level, from top to bottom, with each depth grouped into its own array.

That wording matters. We are not being asked to visit left before right or right before left in some depthfirst sense. We are being asked to process the tree in layers. That is exactly what breadthfirst search is for.


What the Problem is Really Asking

If the tree looks like this:

    3   / \  9  20    /  \   15   7

the expected output is:

[[3], [9, 20], [15, 7]]

That means:

  • depth 0 contains [3]
  • depth 1 contains [9, 20]
  • depth 2 contains [15, 7]

The important point is that each level must stay grouped. That is why a queue is more natural than a stack here.


Why Breadth‑First Search Fits so Neatly

Breadthfirst search processes nodes in the order they are discovered across each depth. In a tree, that means:

  • start with the root
  • visit every node on the current level
  • enqueue their children
  • then move to the next level

That is almost the problem statement turned into code.

Depthfirst traversal is still useful in trees, but it is not the most direct fit when the output itself is organised by level. With DFS, we have to carry level information along and group values afterwards. With BFS, the grouping falls out of the traversal naturally.


A Practical TypeScript Solution

type TreeNode = {  val: number;  left: TreeNode | null;  right: TreeNode | null;};export const levelOrder = (root: TreeNode | null): number[][] => {  if (!root) {    return [];  }  const result: number[][] = [];  const queue: TreeNode[] = [root];  let head = 0;  while (head < queue.length) {    const levelSize = queue.length - head;    const levelValues: number[] = [];    for (let count = 0; count < levelSize; count += 1) {      const node = queue[head];      head += 1;      levelValues.push(node.val);      if (node.left) {        queue.push(node.left);      }      if (node.right) {        queue.push(node.right);      }    }    result.push(levelValues);  }  return result;};

Why the Queue Logic Works

The subtle detail is levelSize.

At the start of each outer loop, queue.length head tells us how many nodes currently belong to this level. That matters because as we process them, we will add their children to the queue. Those children belong to the next level, not the current one.

So the algorithm deliberately freezes the size of the current layer before stepping through it.

That gives us a clean rhythm:

  • measure the current level
  • process exactly that many nodes
  • collect their values together
  • let their children wait in the queue for the next round

Once that clicks, the rest of the solution is fairly mechanical.


Why I Prefer a Head Pointer in JavaScript

You will often see queue solutions written with shift(). That is fine for small examples, but in JavaScript shift() has to reindex the array each time, which can become unnecessarily expensive.

Using a head pointer keeps the queue behaviour explicit without that overhead. It is still just an array, but we read it like a queue:

  • queue[head] is the front item
  • increment head when that item has been consumed

That keeps the code practical without adding much ceremony.


Dfs Can Still Solve It, but Less Directly

There is nothing wrong with a recursive depthfirst version if we pass the current level into the function and push values into result[level].

That approach works, but the reason I would not lead with it here is simple: the problem is already asking for level order, and BFS is level order. When the traversal pattern matches the output format that closely, it is usually worth taking the direct route.


Common Mistakes

Mixing Nodes from Two Levels Together

If we keep reading from the queue until it is empty without freezing the current level size first, the grouping breaks. We need a level boundary.

Forgetting the Empty‑Tree Case

If root is null, the right answer is an empty array, not [[]].

Reaching for Recursion by Habit

Recursion is not wrong here. It is just not the clearest first explanation for a problem whose whole shape is breadthfirst.


Complexity and What Makes This Reliable

Each node is visited once, so the time complexity is O(n). The queue holds up to one full level at a time, so the space complexity is O(w), where w is the maximum width of the tree.

That is exactly the tradeoff we expect from BFS: clear level ordering in exchange for queue storage.

Once plain levelorder traversal makes sense, the zigzag variant is a nice next step because the core idea is similar but the output logic changes. I walked through that in Solving the LeetCode "Binary Tree Zigzag Level Order Traversal" Problem.


Wrapping up

Binary Tree Level Order Traversal is a very tidy BFS problem because the traversal order and the requested output are aligned from the start. We are not using breadthfirst search because it is fashionable. We are using it because the problem is already asking us to think in layers.

Key Takeaways

  • Level order traversal is naturally a breadthfirst search problem.
  • The queue represents nodes waiting to be processed level by level.
  • Freezing the current level size is what keeps the grouping correct.

Once we describe the task as "process the tree one depth at a time", the queue solution stops feeling like a pattern to remember and starts feeling like the obvious way to do the job.


Categories:

  1. Algorithms
  2. Data Structures
  3. Development
  4. Guides
  5. JavaScript
  6. LeetCode
  7. TypeScript