Pattern 11 of 24 · Trees · 25 min

Tree Breadth-First Search

Process a tree level by level with a queue, which makes level sums, views and minimum depths natural.

Spot it:level ordereach level / rowminimum depthleft or right side viewJavaScript you'll use: Stacks, queues and deques Linked list and tree nodes
Say it in 30 seconds
  • O(n)Time: each node enqueued and dequeued once
  • O(w)Space: widest level, up to about n / 2
  • O(n²)Worst case if you dequeue with shift()
Show what to say

“When the answer is per level, visible from one side, or the nearest something, I go breadth-first with a queue. I snapshot the queue size when a level starts, so I know exactly where it ends, and I can stop at the first match. It is O(n) time and O(w) space for the widest level.”

Trade-offs

  • BFS vs DFS for levels: BFS holds one level (about n / 2) and can stop early; DFS holds one path, n if skewed.
  • Head index vs shift(): queue[head++] is O(1); shift() moves every element and can turn BFS into O(n²) on large queues.
  • Reverse once vs unshift per level: for bottom-up levels run normal BFS and reverse the result once, O(n), rather than unshift each level.

Bring it up before they ask

Loop on the size you snapshotted, not the live queue length, or the levels run together; and never enqueue null children.

0 of 0 sections ticked
30-second answer

Breadth-first search reads a tree level by level with a first-in, first-out queue. Snapshot the queue size when a level starts and you know where it ends. Shallow nodes come first, so BFS can stop at the first match. O(n) time.

When to reach for it

Spot it in the problem0 of 4

Tap the signals you can see in the problem.

Rememberper levelfrom one sidenearesttop to bottom, left to right

The core idea: a queue keeps the levels in order

Drop a stone in a pond: the ripples spread one ring at a time.

Read more · 82 words

A queue does the same to a tree. Take a node out of the front, put its children at the back. They wait behind every node of the current level, so nodes come out in depth order.

One rule follows:

When a level starts, the queue holds exactly that level, and nothing else.

So read size then, and take out exactly size nodes. Whatever they add is the next level. Watch the shaded band (this level) and the new arrivals (the next):

A queue is a line at a ticket window: people wait in order and are served one at a time.

Queue + level-size loop: the largest value on each level

tree
8316471014
queue (front first) (1)
8
front = back
result (0)
empty
head
0
size
"?"
max
"-∞"

Put the root in the queue. The queue will always hold the rest of the current level, followed by the start of the next one.

function levelMaxes(root) {
  if (root === null) return [];
  const result = [], queue = [root];
  let head = 0;
  while (head < queue.length) {
    const size = queue.length - head;
    let max = -Infinity;
    for (let i = 0; i < size; i++) {
      const node = queue[head++];
      max = Math.max(max, node.val);
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
    result.push(max);
  }
  return result;
}
All steps as text
  1. Put the root in the queue. The queue will always hold the rest of the current level, followed by the start of the next one.
  2. Level 1 starts. Exactly 1 node is waiting, and it is the whole level: snapshot size = 1 before anything new is added.
  3. Take 8 from the front (head moves to 1); max on this level is now 8. Its children 3 and 10 join the back, behind the rest of level 1.
  4. The only node of level 1 is done, so record 8. Whatever is in the queue now is exactly level 2.
  5. Level 2 starts. Exactly 2 nodes are waiting, and they are the whole level: snapshot size = 2 before anything new is added.
  6. Take 3 from the front (head moves to 2); max on this level is now 3. Its children 1 and 6 join the back, behind the rest of level 2.
  7. Take 10 from the front (head moves to 3); max on this level is now 10. Its child 14 joins the back, behind the rest of level 2.
  8. All 2 nodes of level 2 are done, so record 10. Whatever is in the queue now is exactly level 3.
  9. Level 3 starts. Exactly 3 nodes are waiting, and they are the whole level: snapshot size = 3 before anything new is added.
  10. Take 1 from the front (head moves to 4); max on this level is now 1. It has no children to enqueue.
  11. Take 6 from the front (head moves to 5); max on this level is now 6. Its children 4 and 7 join the back, behind the rest of level 3.
  12. Take 14 from the front (head moves to 6); max on this level is now 14. It has no children to enqueue.
  13. All 3 nodes of level 3 are done, so record 14. Whatever is in the queue now is exactly level 4.
  14. Level 4 starts. Exactly 2 nodes are waiting, and they are the whole level: snapshot size = 2 before anything new is added.
  15. Take 4 from the front (head moves to 7); max on this level is now 4. It has no children to enqueue.
  16. Take 7 from the front (head moves to 8); max on this level is now 7. It has no children to enqueue.
  17. All 2 nodes of level 4 are done, so record 7. Nothing was enqueued, so this was the last level.
  18. The queue is empty: every node was enqueued once and dequeued once, O(n). The largest value on each level: [8, 10, 14, 7].

Play with it

Run the ripple and watch each level take its colour.

Read more · 53 words
  1. Handle the empty tree

    Return [] or 0 when root is null.

  2. Seed the queue

    Root in an array, plus a head index.

  3. Snapshot the level size

    size = queue.length - head.

  4. Process the level

    Dequeue size nodes, then enqueue their children.

  5. Record or stop

    Push the level's answer, or return at the first match.

Interactive

Level-by-level ripple

A queue hands out nodes in the order they joined, so the tree is read one level at a time. Click + to grow a node, or a node to prune it.

8310161447queuefront →8
Levels:none yet
Dequeued 0/8Queue now 1 (at most the widest level: O(w) space)

The queue starts with the root. Press Run or Step.

Other trees

Templates

Template 1: the level-size loop

Every level problem is a small edit of this loop:

Read more · 32 words
// Template: BFS with a level-size loop. At the start of each level, the queue holds exactly that level's nodes,
// so snapshot its size and process that many; everything they enqueue belongs to the next level.
export function levelMaxes(root) {
  const result = [];
  const queue = root ? [root] : [];
  let head = 0;                                // queue[head] is the front: no shift(), so dequeue is O(1)
  while (head < queue.length) {
    const size = queue.length - head;          // nodes waiting right now = one full level
    let max = -Infinity;
    for (let i = 0; i < size; i++) {
      const node = queue[head++];
      max = Math.max(max, node.val);           // per-level work goes here: sum, count, first, last…
      if (node.left) queue.push(node.left);    // children line up behind the rest of this level
      if (node.right) queue.push(node.right);
    }
    result.push(max);
  }
  return result;
}

// The same loop with two arrays: build the next level while walking the current one, then swap.
export function levelValues(root) {
  const levels = [];
  let level = root ? [root] : [];
  while (level.length) {
    levels.push(level.map((node) => node.val));
    const next = [];
    for (const node of level) {
      if (node.left) next.push(node.left);
      if (node.right) next.push(node.right);
    }
    level = next;
  }
  return levels;
}

Template 2: stop at the first match

BFS meets every node at depth d before any at depth d + 1, so the first match it dequeues is a shallowest one. DFS would have to explore the whole tree.

Minimum Depth of Binary TreeEasy is shallowestDepth(root, isLeaf). A leaf has no children.

Early exit: the first leaf BFS meets is the shallowest

tree
12467583
queue array (1)
1
▲ head
head
0
depth
0

The queue is a plain array plus a head index. Dequeuing reads queue[head] and moves head forward; the array itself is never shifted, so each dequeue is O(1).

function minDepth(root) {
  if (root === null) return 0;
  const queue = [root];
  let head = 0, depth = 0;
  while (head < queue.length) {
    depth++;
    const size = queue.length - head;
    for (let i = 0; i < size; i++) {
      const node = queue[head++];
      if (!node.left && !node.right) return depth;
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }
}
All steps as text
  1. The queue is a plain array plus a head index. Dequeuing reads queue[head] and moves head forward; the array itself is never shifted, so each dequeue is O(1).
  2. Depth 1 is just the root. If it has no children, the answer is 1.
  3. Dequeue: read queue[0] = 1 and move head to 1. The slots before head stay in the array but are out of play.
  4. 1 is not a leaf. Push 2 and 3 onto the end of the array for the next depth.
  5. Depth 2: the 2 nodes from head to the end of the array are this level. Every shallower node has been checked, and none of them was a leaf.
  6. Dequeue: read queue[1] = 2 and move head to 2. The slots before head stay in the array but are out of play.
  7. 2 is not a leaf. Push 4 and 5 onto the end of the array for the next depth.
  8. Dequeue: read queue[2] = 3 and move head to 3. The slots before head stay in the array but are out of play.
  9. 3 has no children: it is a leaf, and BFS reaches depths in increasing order, so no leaf can be shallower. Return 2. The 5 faded nodes are never visited.
// Early exit: BFS meets nodes in order of depth, so the first node that matches is a shallowest one.
// Return as soon as you see it: nothing deeper can beat it, so the rest of the tree is never touched.
export function shallowestDepth(root, matches) {
  const queue = root ? [root] : [];
  let head = 0;
  let depth = 0;
  while (head < queue.length) {
    depth++;
    const size = queue.length - head;
    for (let i = 0; i < size; i++) {
      const node = queue[head++];
      if (matches(node)) return depth;
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }
  return -1;                                   // nothing matched
}

export const isLeaf = (node) => node.left === null && node.right === null;

Template 3: a queue that never shifts

shift() moves every element down a slot, so big queues can turn O(n²). Instead use a head index (queue[head++]), swap two arrays per level, or write a small Queue class.

// A queue on top of an array and a head index. push() is O(1) and so is shift() here: it only moves head.
// Array.prototype.shift() moves every remaining element down one slot, so a big BFS that uses it is O(n²).
export class Queue {
  #items = [];
  #head = 0;

  push(item) {
    this.#items.push(item);
  }

  shift() {
    if (this.#head === this.#items.length) return undefined;
    const item = this.#items[this.#head++];
    if (this.#head > 1024 && this.#head * 2 > this.#items.length) {
      this.#items = this.#items.slice(this.#head); // now and then, drop the used prefix to free memory
      this.#head = 0;
    }
    return item;
  }

  get size() {
    return this.#items.length - this.#head;
  }
}

Try it

try-it.js
// Edit the trees (level order, null = missing child) and press Run. The functions are the templates above.
const root = fromArray([8, 3, 10, 1, 6, null, 14, null, null, 4, 7]);
console.log(levelValues(root));                 // [[8], [3, 10], [1, 6, 14], [4, 7]]
console.log(levelMaxes(root));                  // [8, 10, 14, 7]
console.log(shallowestDepth(root, isLeaf));     // 3 (leaves 1 and 14 are on level 3)

// shift() vs a head index on a 50,000-item queue. Double the length and watch shift() take about 4x as long.
const items = Array.from({ length: 50000 }, (_, i) => i);
let t = Date.now();
const a = [...items];
while (a.length) a.shift();
console.log('shift():', Date.now() - t, 'ms');    // around 100 ms, and it grows quadratically
t = Date.now();
const q = new Queue();
for (const x of items) q.push(x);
while (q.size) q.shift();
console.log('head index:', Date.now() - t, 'ms'); // a few ms at most

Complexity and JavaScript you'll use

Complexity

Read more · 93 words
VariantTimeExtra space
Level-size BFSO(n): each node enqueued and dequeued onceO(w): the widest level, up to about n / 2
Early exit (first match)O(n) worst case, often far lessO(w) for the levels it reaches
BFS with shift()O(n²) worst case on large queuesO(w)
DFS passing the depth downO(n)O(h) for the recursion

JavaScript you'll use

JavaScript refresher: Stacks, queues and deques Linked list and tree nodes Arrays: create, read, update
const queue = root ? [root] : [];       // a queue: push at the back, read at head
let head = 0;
const size = queue.length - head;       // snapshot: the nodes of the current level
const node = queue[head++];             // O(1) dequeue, nothing moves
if (node.left) queue.push(node.left);   // never enqueue null children
level[size - 1 - i] = node.val;         // fill from the back for a right-to-left level
const avg = sum / size;                 // / is always floating-point division in JS

Interview questions

BFS or DFS for a level-based question?

Both work. BFS holds one level and can stop early; DFS holds one path, which is n for a skewed tree.

How do you return the levels bottom-up?

Run the normal BFS and reverse the list of levels once at the end.

How do you link every node to its right neighbour on the same level?

In the level-size loop, link each node to the next one unless it is last in its level.

How would you find the maximum width of a level, counting the gaps?

Number positions like a heap (children of p at 2p + 1 and 2p + 2). Width is last minus first plus one.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. In the level-size loop, why read size = queue.length - head before the inner loop instead of looping while i < queue.length - head?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

Next: A tree with up to 26 children per node is a trie, built for prefixes.