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

1

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.

ItemBFSDFS
Data structureQueue (FIFO) — first in, first outStack (LIFO) / call stack for recursion
Traversal orderLevel by level — all nodes at depth d before d+1Branch 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 nodesO(depth) — worst case: O(n) for skewed tree
Finds closest first✅ Yes — nearest neighbors first❌ No — may find distant nodes first
Use casesShortest path, level-order, nearest neighbor, word ladderTopological sort, cycle detection, backtracking, DFS components
2

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.

javascriptBFS Level-Order Traversal — LeetCode #102
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]
3

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.

javascriptBFS Shortest Path — Unweighted Graph
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
}
4

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.

javascriptShortest Distance from Each Cell to Nearest 0 — LeetCode #542
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 0
5

Efficient Queue for BFS in JavaScript

Array.shift() is O(n) — use a pointer for large BFS

JavaScript's array shift() moves all elements left — O(n) per dequeue. For BFS on large graphs, this makes the total complexity O(V²) instead of O(V+E). Use an index pointer to simulate dequeue in O(1).
javascriptO(1) Queue with Index Pointer
// ❌ 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; }
}
6

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.

7

BFS Implementation Checklist

1

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.

2

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.

3

Process queue until empty

while (queue.length > 0) or while (head < queue.length). Each iteration: dequeue front, process it, enqueue unvisited neighbors.

4

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.

5

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.

Frequently Asked Questions