Breadth-First Search

Explore a graph in waves with a queue to find the fewest-steps path from a start node, on adjacency lists and on grids. Traced, with JS and Python.

16 minLesson 2 of 4
On this page
  1. The idea: a queue keeps the rings in order
  2. Trace it on the bus map
  3. The code
  4. BFS on a grid
  5. Common mistakes
  6. Check yourself
  7. What to practise next

Drop a stone into a still pond. A ring spreads out, then a bigger one, then a bigger one. Every point the first ring touches is one unit from the stone, the second ring two units. The rings never reach a far point before a near one.

Breadth-first search (BFS) spreads through a graph the same way. Start at one node. Visit everything one edge away, then everything two edges away, and so on. Because it goes out in rings, the first time BFS reaches a node is by the shortest route. That is why you use it: in an unweighted graph, BFS finds the path with the fewest edges.

The idea: a queue keeps the rings in order

You need a way to finish ring 1 before starting ring 2. A queue does that for free. Take a node from the front, and put its new neighbours at the back. The neighbours wait behind everything that was already in line, which is everything closer to the start.

You also need to remember which nodes you have already found. As Graph Basics warned, a graph can have cycles, so without a memory you would walk around in circles. Keep a set of seen nodes, and add a node to it the moment it joins the queue.

  1. Put the start node in the queue. Mark it seen, at distance 0.
  2. Take the node at the front of the queue.
  3. For each neighbour not yet seen: mark it seen, give it distance (this node's distance + 1), and add it to the back.
  4. Repeat until the queue is empty.

Trace it on the bus map

Here is the bus map from Graph Basics again, starting from A, with neighbour lists in alphabetical order. Watch the queue and the d= labels.

BFS from A: fewest stops to every stopUse the arrows or ← → keys
queue = [start]; dist[start] = 0while queue is not empty:  node = take from the front of queue  for each neighbour next of node:    if next not seen:      dist[next] = dist[node] + 1      add next to the back of queue
  1. queue
    0A
    Ad=0BCDEFG

    1Put A in the queue and mark it seen, at distance 0. Seen means "already in line or done", so nothing gets added twice.

  2. queue
    0B
    1C
    Ad=0Bd=1Cd=1DEFG
    visited
    1

    2Take A from the front (d=0). New neighbours B and C join the back at d=1.

  3. queue
    0C
    1D
    Ad=0Bd=1Cd=1Dd=2EFG
    visited
    2

    3Take B from the front (d=1). New neighbour D joins the back at d=2. A is already seen, so skip it.

  4. queue
    0D
    1E
    Ad=0Bd=1Cd=1Dd=2Ed=2FG
    visited
    3

    4Take C from the front (d=1). New neighbour E joins the back at d=2. A and D are already seen, so skip them. D was reached through B at d=2, and going through C would also give 2, so nothing is lost.

  5. queue
    0E
    1F
    Ad=0Bd=1Cd=1Dd=2Ed=2Fd=3G
    visited
    4

    5Take D from the front (d=2). New neighbour F joins the back at d=3. B and C are already seen, so skip them.

  6. queue
    0F
    Ad=0Bd=1Cd=1Dd=2Ed=2Fd=3G
    visited
    5

    6Take E from the front (d=2). Both neighbours, C and F, are already seen. Nothing joins the queue.

  7. queue
    0G
    Ad=0Bd=1Cd=1Dd=2Ed=2Fd=3Gd=4
    visited
    6

    7Take F from the front (d=3). New neighbour G joins the back at d=4. D and E are already seen, so skip them.

  8. queue
    Ad=0Bd=1Cd=1Dd=2Ed=2Fd=3Gd=4
    visited
    7

    8Take G from the front (d=4). F is already seen, so skip it.

  9. queue
    Ad=0Bd=1Cd=1Dd=2Ed=2Fd=3Gd=4
    visited
    7

    9Queue empty. Every stop has its fewest-stops distance. G is 4 stops from A: G came from F, F from D, D from B, B from A.

1 / 9

Look at the order nodes left the queue: A, B, C, D, E, F, G. That is distance 0, then 1, 1, then 2, 2, then 3, then 4. The queue never holds two nodes whose distances differ by more than 1, and the closer ones are always at the front. That is why the first distance BFS gives a node is final.

The code

Along with the distance, store each node's parent: the node it was discovered from. To recover the actual route, start at the goal and follow parents back to the start, then reverse.

// graph: adjacency list, e.g. { A: ["B", "C"], B: ["A", "D"], ... }
function bfs(graph, start) {
  const dist = { [start]: 0 }; // also our "seen" set
  const parent = { [start]: null };
  const queue = [start];
  let head = 0; // index of the front: O(1) dequeue
  while (head < queue.length) {
    const node = queue[head++];
    for (const next of graph[node]) {
      if (next in dist) continue; // already seen
      dist[next] = dist[node] + 1;
      parent[next] = node;
      queue.push(next);
    }
  }
  return { dist, parent };
}
 
function pathTo(parent, goal) {
  if (!(goal in parent)) return null; // unreachable
  const path = [];
  for (let at = goal; at !== null; at = parent[at]) path.push(at);
  return path.reverse();
}
// pathTo(bfs(graph, "A").parent, "G") → ["A", "B", "D", "F", "G"]
from collections import deque
 
# graph: adjacency list, e.g. {"A": ["B", "C"], "B": ["A", "D"], ...}
def bfs(graph, start):
    dist = {start: 0}  # also our "seen" set
    parent = {start: None}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for nxt in graph[node]:
            if nxt in dist:
                continue  # already seen
            dist[nxt] = dist[node] + 1
            parent[nxt] = node
            queue.append(nxt)
    return dist, parent
 
def path_to(parent, goal):
    if goal not in parent:
        return None  # unreachable
    path = []
    at = goal
    while at is not None:
        path.append(at)
        at = parent[at]
    return path[::-1]
# path_to(bfs(graph, "A")[1], "G") → ['A', 'B', 'D', 'F', 'G']

A, C, E, F, G is also 4 stops. BFS returns whichever shortest path it met first, which depends on the order of the neighbour lists. Both are correct answers.

TimeO(V + E)
SpaceO(V)

Each node joins the queue at most once, and each neighbour list is read once, so every edge is looked at twice in an undirected graph. The queue, dist and parent hold at most V entries.

BFS on a grid

The fewest moves through a maze is the same problem. As Graph Basics showed, each open cell is a node and its neighbours are the open cells up, down, left and right. Instead of a dist map, use a 2D array of the same size as the grid, filled with -1 for "not seen yet".

Fewest moves from the top-left corner to the exit GUse the arrows or ← → keys
  1. steps from start
    01234
    00#
    1#
    2##
    3#G
    distance
    0
    cells reached
    1

    1Start in the top-left corner at distance 0. Walls (#) are not part of the graph.

  2. steps from start
    01234
    001#
    11#
    2##
    3#G
    distance
    1
    cells reached
    3

    2Wave 1: the open cells one step away. Right and down both work; up and left are outside the grid.

  3. steps from start
    01234
    0012#
    11#
    22##
    3#G
    distance
    2
    cells reached
    5

    3Wave 2. Each new cell gets its parent's distance plus 1. The wall in column 1 blocks the middle.

  4. steps from start
    01234
    0012#
    11#3
    22##
    33#G
    distance
    3
    cells reached
    7

    4Wave 3. Two separate fronts move at the same pace: one along the top, one down the left side.

  5. steps from start
    01234
    0012#
    11#34
    22#4#
    334#G
    distance
    4
    cells reached
    10

    5Wave 4: (1, 3), (2, 2) and (3, 1). Two of the fronts are now one step from the same cell, (3, 2).

  6. steps from start
    01234
    0012#
    11#345
    22#4#
    3345#G
    distance
    5
    cells reached
    12

    6Wave 5. Both (2, 2) and (3, 1) touch (3, 2). Whichever leaves the queue first claims it at distance 5; the other finds it already seen and skips it.

  7. steps from start
    01234
    0012#6
    11#345
    22#4#6
    3345#G
    distance
    6
    cells reached
    14

    7Wave 6. The right-hand column fills in from (1, 4), going up and down at the same time.

  8. steps from start
    01234
    0012#6
    11#345
    22#4#6
    3345#7
    distance
    7
    cells reached
    15

    8Wave 7 reaches the exit G. Because cells come out of the queue in distance order, no shorter route can exist.

  9. steps from start
    01234
    0012#6
    11#345
    22#4#6
    3345#7
    answer
    7
    cells reached
    15

    9Answer: 7 steps. BFS took 15 cells out of the queue, each once, and looked at each cell's 4 sides once.

1 / 9
const DIRS = [[-1, 0], [1, 0], [0, -1], [0, 1]];
 
// grid: array of strings, "#" is a wall. Start top-left, exit bottom-right.
function stepsToExit(grid) {
  const rows = grid.length, cols = grid[0].length;
  const dist = grid.map((row) => new Array(row.length).fill(-1));
  dist[0][0] = 0;
  const queue = [[0, 0]];
  let head = 0;
  while (head < queue.length) {
    const [r, c] = queue[head++];
    if (r === rows - 1 && c === cols - 1) return dist[r][c];
    for (const [dr, dc] of DIRS) {
      const nr = r + dr, nc = c + dc;
      if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;
      if (grid[nr][nc] === "#" || dist[nr][nc] !== -1) continue;
      dist[nr][nc] = dist[r][c] + 1;
      queue.push([nr, nc]);
    }
  }
  return -1; // the exit cannot be reached
}
 
stepsToExit(["...#.", ".#...", ".#.#.", "...#."]); // 7
from collections import deque
 
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
 
# grid: list of strings, "#" is a wall. Start top-left, exit bottom-right.
def steps_to_exit(grid):
    rows, cols = len(grid), len(grid[0])
    dist = [[-1] * cols for _ in range(rows)]
    dist[0][0] = 0
    queue = deque([(0, 0)])
    while queue:
        r, c = queue.popleft()
        if (r, c) == (rows - 1, cols - 1):
            return dist[r][c]
        for dr, dc in DIRS:
            nr, nc = r + dr, c + dc
            if not (0 <= nr < rows and 0 <= nc < cols):
                continue
            if grid[nr][nc] == "#" or dist[nr][nc] != -1:
                continue
            dist[nr][nc] = dist[r][c] + 1
            queue.append((nr, nc))
    return -1  # the exit cannot be reached
 
steps_to_exit(["...#.", ".#...", ".#.#.", "...#."])  # 7

A grid with R rows and C columns has R × C nodes and at most 4 edges each, so this is O(R × C) time and space.

Common mistakes

Check yourself

Quick check

Graph: { P: [Q, R], Q: [P, S], R: [P, S, T], S: [Q, R], T: [R] }. In what order does BFS from P take nodes out of the queue?

Why does BFS mark a node as seen when it is added to the queue, not when it is taken out?

A grid has 1,000 rows and 1,000 columns. Roughly how many cells can BFS take out of the queue in the worst case?

What to practise next

Depth-First Search walks the same bus map in a very different order: one branch as deep as it goes before backing up. It cannot find shortest paths, but it is the simpler tool for "is it connected?" and "how many separate pieces are there?".