Breadth-First Search (BFS) Explained With Easy Tree Examples
BFS explores all neighbors at the current depth before going deeper — like ripples spreading outward from a stone dropped in water. It's the algorithm behind shortest paths in unweighted graphs, level-order tree traversal, "degrees of separation" problems, and multi-source flood fill. This guide covers BFS on trees, graphs, grids, and explains the key patterns used in coding interviews with complete JavaScript implementations and step-by-step walkthroughs.
O(V+E)
time — visits all vertices and edges exactly once
Queue
BFS uses a queue (FIFO) — the core data structure
Shortest path
guaranteed in unweighted graphs
Level-order
natural tree traversal — processes nodes level by level
BFS vs DFS — The Key Difference
The queue vs stack insight
BFS = queue (FIFO). DFS = stack (LIFO). BFS explores level by level — all nodes at depth 1, then depth 2, etc. DFS goes as deep as possible first along one branch before backtracking. BFS guarantees the shortest path in unweighted graphs because it reaches each node via the minimum number of edges; DFS does not.
| Item | BFS | DFS |
|---|---|---|
| Data structure | Queue (FIFO) — first in, first out | Stack (LIFO) / call stack for recursion |
| Traversal order | Level by level — all nodes at depth d before d+1 | Branch by branch — fully explores one path before backtracking |
| Shortest path | ✅ Yes (unweighted graphs) | ❌ Not guaranteed — may find long path first |
| Memory (tree) | O(width) — worst case: last level has n/2 nodes | O(depth) — worst case: O(n) for skewed tree |
| Finds closest first | ✅ Yes — nearest neighbors first | ❌ No — may find distant nodes first |
| Use cases | Shortest path, level-order, nearest neighbor, word ladder | Topological sort, cycle detection, backtracking, DFS components |
BFS on a Tree — Level-Order Traversal
Level-order traversal visits nodes level by level from top to bottom, left to right. The key technique: track how many nodes are in the queue at the start of each level — that's levelSize. Process exactly that many nodes, then all their children form the next level.
function levelOrder(root) {
if (!root) return [];
const result = [];
const queue = [root]; // Queue — FIFO
while (queue.length > 0) {
const levelSize = queue.length; // all nodes at the current level
const level = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift(); // dequeue (process current node)
level.push(node.val);
if (node.left) queue.push(node.left); // enqueue left child
if (node.right) queue.push(node.right); // enqueue right child
}
result.push(level); // all nodes at this level are done
}
return result;
}
// 3
// / \
// 9 20
// / \
// 15 7
//
// Step-by-step:
// Queue: [3] → level=[3], enqueue 9,20 → result=[[3]]
// Queue: [9,20] → level=[9,20], enqueue 15,7 → result=[[3],[9,20]]
// Queue: [15,7] → level=[15,7], no children → result=[[3],[9,20],[15,7]]
levelOrder(root); // → [[3], [9,20], [15,7]]
// Variant: Right side view (last node of each level)
function rightSideView(root) {
if (!root) return [];
const result = [];
const queue = [root];
while (queue.length > 0) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
if (i === levelSize - 1) result.push(node.val); // last node = visible from right
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return result;
}
rightSideView(root); // → [3, 20, 7]BFS on a Graph — Shortest Path
BFS in a graph requires tracking visited nodes to avoid infinite loops in cycles. The first time BFS reaches any node, it has found the shortest path to that node (in terms of number of edges). Store the path itself by carrying the path array alongside each node in the queue.
function shortestPath(graph, start, end) {
const visited = new Set([start]);
const queue = [[start, [start]]]; // each entry: [currentNode, pathSoFar]
while (queue.length > 0) {
const [node, path] = queue.shift();
if (node === end) return path; // first time we reach 'end' = shortest path
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, [...path, neighbor]]);
}
}
}
return null; // no path exists
}
const graph = {
A: ['B', 'C'],
B: ['A', 'D', 'E'],
C: ['A', 'F'],
D: ['B'],
E: ['B', 'F'],
F: ['C', 'E']
};
shortestPath(graph, 'A', 'F'); // → ['A', 'C', 'F'] (2 edges, shortest)
// BFS considers A's neighbors: B, C
// Then B's neighbors: D, E (A already visited)
// Then C's neighbors: F ← found! Path: A→C→F
// Shortest distance only (without path):
function shortestDistance(graph, start, end) {
const visited = new Set([start]);
const queue = [[start, 0]]; // [node, distance]
while (queue.length > 0) {
const [node, dist] = queue.shift();
if (node === end) return dist;
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, dist + 1]);
}
}
}
return -1; // unreachable
}BFS on a Grid — 0/1 Matrix
Grid BFS is a special case where the "graph" is implicit — neighbors are the 4 adjacent cells (up/down/left/right). Multi-source BFS initializes all source cells simultaneously in the queue, computing distance from the nearest source for every cell in one pass.
function updateMatrix(mat) {
const m = mat.length, n = mat[0].length;
const dist = Array.from({length: m}, () => new Array(n).fill(Infinity));
const queue = [];
// Multi-source BFS: initialize all 0 cells as sources with distance 0
for (let r = 0; r < m; r++) {
for (let c = 0; c < n; c++) {
if (mat[r][c] === 0) {
dist[r][c] = 0;
queue.push([r, c]);
}
}
}
const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; // down, up, right, left
while (queue.length > 0) {
const [r, c] = queue.shift();
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < m && nc >= 0 && nc < n &&
dist[nr][nc] > dist[r][c] + 1) { // found shorter distance
dist[nr][nc] = dist[r][c] + 1;
queue.push([nr, nc]);
}
}
}
return dist;
}
// Matrix: [[0,0,0],[0,1,0],[1,1,1]]
// Result: [[0,0,0],[0,1,0],[1,2,1]]
// The center bottom cell has distance 2 from nearest 0Efficient Queue for BFS in JavaScript
Array.shift() is O(n) — use a pointer for large BFS
// ❌ Slow for large inputs: queue.shift() is O(n)
while (queue.length > 0) {
const node = queue.shift(); // O(n) — shifts all elements left
// ...
}
// ✅ Fast: use a pointer — O(1) dequeue
const queue = [startNode];
let head = 0; // points to front of queue
while (head < queue.length) {
const node = queue[head++]; // O(1) — just increment pointer
// process node...
for (const neighbor of graph[node]) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor); // O(1) amortized
}
}
}
// For competitive programming, consider a proper deque:
class Deque {
constructor() { this.data = {}; this.head = 0; this.tail = 0; }
push(val) { this.data[this.tail++] = val; }
shift() { return this.data[this.head++]; }
get length() { return this.tail - this.head; }
}Classic BFS Interview Problems
Level-order traversal (LeetCode #102)
Process binary tree level by level. Key: use levelSize = queue.length at start of each level to separate levels. Return [[level1], [level2], ...].
Word ladder (LeetCode #127)
Find shortest transformation from start word to end word, changing one letter at a time. BFS from start word, neighbors = all words differing by one character. Each step = one transformation.
Rotting oranges (LeetCode #994)
Multi-source BFS from all rotten oranges simultaneously. Each minute, rotten oranges infect adjacent fresh ones. Return minutes until all rotten, or -1 if impossible.
Number of islands (LeetCode #200)
BFS from each unvisited "1" cell, mark all connected land cells as visited. Count how many separate BFS runs you start — each is one island.
Clone graph (LeetCode #133)
BFS with HashMap mapping original node → clone node. Clone each node when first seen, copy neighbors once all clones exist. Return clone of start node.
Bipartite graph check (LeetCode #785)
BFS with 2-coloring. Color each node alternating colors. If you ever try to color a node with the same color as its neighbor — graph is not bipartite.
BFS Implementation Checklist
Initialize queue with starting node(s)
For single-source BFS: queue = [start]. For multi-source: queue = [all sources]. Mark all initial nodes as visited immediately.
Mark visited on enqueue, not dequeue
Always add nodes to visited when pushing to queue, not when popping. Otherwise the same node gets enqueued multiple times, causing O(V²) work in dense graphs.
Process queue until empty
while (queue.length > 0) or while (head < queue.length). Each iteration: dequeue front, process it, enqueue unvisited neighbors.
Track what you need: path vs distance vs level
For distance: carry integer. For path: carry array. For level: use levelSize = queue.length pattern at start of each while iteration.
Handle disconnected graphs
For graphs that may not be fully connected, run BFS from every unvisited node (outer loop over all nodes). Each BFS run finds one connected component.