Tree Breadth-First Search
Process a tree level by level with a queue, which makes level sums, views and minimum depths natural.
- 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.
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
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):
Queue + level-size loop: the largest value on each level
- 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
- 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.
- Level 1 starts. Exactly 1 node is waiting, and it is the whole level: snapshot size = 1 before anything new is added.
- 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.
- The only node of level 1 is done, so record 8. Whatever is in the queue now is exactly level 2.
- Level 2 starts. Exactly 2 nodes are waiting, and they are the whole level: snapshot size = 2 before anything new is added.
- 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.
- 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.
- All 2 nodes of level 2 are done, so record 10. Whatever is in the queue now is exactly level 3.
- Level 3 starts. Exactly 3 nodes are waiting, and they are the whole level: snapshot size = 3 before anything new is added.
- Take 1 from the front (head moves to 4); max on this level is now 1. It has no children to enqueue.
- 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.
- Take 14 from the front (head moves to 6); max on this level is now 14. It has no children to enqueue.
- All 3 nodes of level 3 are done, so record 14. Whatever is in the queue now is exactly level 4.
- Level 4 starts. Exactly 2 nodes are waiting, and they are the whole level: snapshot size = 2 before anything new is added.
- Take 4 from the front (head moves to 7); max on this level is now 4. It has no children to enqueue.
- Take 7 from the front (head moves to 8); max on this level is now 7. It has no children to enqueue.
- All 2 nodes of level 4 are done, so record 7. Nothing was enqueued, so this was the last level.
- 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
Handle the empty tree
Return
[]or0whenrootisnull.Seed the queue
Root in an array, plus a
headindex.Snapshot the level size
size = queue.length - head.Process the level
Dequeue
sizenodes, then enqueue their children.Record or stop
Push the level's answer, or return at the first match.
Templates
Template 1: the level-size loop
Every level problem is a small edit of this loop:
Read more · 32 words
- Average of Levels in Binary TreeEasy: keep a running sum.
- Binary Tree Level Order TraversalMedium: push each value into a
levelarray. - Binary Tree Right Side ViewMedium: keep only the node at
i === size - 1. - Binary Tree Zigzag Level Order TraversalMedium: on every other level, write at
size - 1 - i.
// 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
- 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
- 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).
- Depth 1 is just the root. If it has no children, the answer is 1.
- Dequeue: read queue[0] = 1 and move head to 1. The slots before head stay in the array but are out of play.
- 1 is not a leaf. Push 2 and 3 onto the end of the array for the next depth.
- 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.
- Dequeue: read queue[1] = 2 and move head to 2. The slots before head stay in the array but are out of play.
- 2 is not a leaf. Push 4 and 5 onto the end of the array for the next depth.
- Dequeue: read queue[2] = 3 and move head to 3. The slots before head stay in the array but are out of play.
- 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
// 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 mostComplexity and JavaScript you'll use
Complexity
Read more · 93 words
| Variant | Time | Extra space |
|---|---|---|
| Level-size BFS | O(n): each node enqueued and dequeued once | O(w): the widest level, up to about n / 2 |
| Early exit (first match) | O(n) worst case, often far less | O(w) for the levels it reaches |
BFS with shift() | O(n²) worst case on large queues | O(w) |
| DFS passing the depth down | O(n) | O(h) for the recursion |
JavaScript you'll use
JavaScript refresher: Stacks, queues and deques Linked list and tree nodes Arrays: create, read, updateconst 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 JSInterview 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 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Average of Levels in Binary TreeReturn the average value of the nodes on each level of a binary tree, from the top level down.Easy
- Minimum Depth of Binary TreeCount the nodes on the shortest path from the root down to a leaf.Easy
- Binary Tree Level Order TraversalGroup the values of a binary tree by level: top level first, left to right within each level.Medium
- Binary Tree Right Side ViewList the values you would see standing to the right of a binary tree, one per level from top to bottom.Medium
- Binary Tree Zigzag Level Order TraversalGroup a binary tree's values by level, reading the first level left to right, the next right to left, and so on.Medium
- Serialize and Deserialize Binary TreeTurn a binary tree into a string and rebuild the identical tree from that string.Hard
- Maximum Depth of Binary TreeCount the nodes on the longest path from the root down to a leaf.Easy
- Invert Binary TreeMirror a binary tree by swapping the left and right child of every node.Easy