Breadth-first search (BFS)

Breadth-first search explores a graph level by level using a queue. It finds the shortest path in unweighted graphs in O(V + E) and is the foundation of many graph algorithms (web crawling, social-network distance, network broadcast).

1. Overview

BFS Visits the Example Graph Level by Level ABCDEFLevel 0Level 1Level 2Level 3Shortest path A-B-D-F = 3 edges

BFS answers the question "what can I reach, and in how few steps?" Starting from a source vertex it visits every vertex at distance 1, then every vertex at distance 2, and so on. The diagram shows the example graph used in the code below: A is level 0, B and C are level 1, D and E are level 2, and F is level 3. Because every vertex is first reached through a shortest route, recording each vertex's parent when it is discovered gives a shortest-path tree for the whole graph, not only for one target.

The key data structure is a FIFO queue. Vertices enter the queue in order of distance, so the queue never contains vertices more than one level apart. This is what separates BFS from depth-first search, which uses a stack (or recursion) and dives along one branch before backtracking.

2. Complexity

Best Average Worst Space (Worst)
Ω(V+E) Θ(V+E) O(V+E) O(V)

3. How it works

Start with the source in a queue and a visited set. Repeatedly: pop the front node, visit its unvisited neighbors, mark them visited, and enqueue them. The queue ensures nodes are processed in order of distance from the source.

4. Python implementation

from collections import deque


def bfs(graph, start):
    """BFS from start; yields nodes in BFS order."""
    visited = {start}
    q = deque([start])
    while q:
        node = q.popleft()
        yield node
        for nbr in graph[node]:
            if nbr not in visited:
                visited.add(nbr)
                q.append(nbr)


def shortest_path(graph, start, goal):
    """Shortest unweighted path from start to goal."""
    if start == goal: return [start]
    parents = {start: None}
    q = deque([start])
    while q:
        node = q.popleft()
        for nbr in graph[node]:
            if nbr in parents: continue
            parents[nbr] = node
            if nbr == goal:
                path = [goal]
                while parents[path[-1]] is not None:
                    path.append(parents[path[-1]])
                return path[::-1]
            q.append(nbr)
    return None


graph = {
    "A": ["B", "C"], "B": ["D"], "C": ["D", "E"], "D": ["F"], "E": ["F"], "F": [],
}
print(list(bfs(graph, "A")))            # ['A', 'B', 'C', 'D', 'E', 'F']
print(shortest_path(graph, "A", "F"))   # ['A', 'B', 'D', 'F']

5. Trade-offs

6. Step-by-Step Trace

Queue Trace: bfs(graph, "A") StepActionQueue after stepVisited set0pop -A{A}1pop ABC{A,B,C}2pop BCD{A,B,C,D}3pop CDE{..,E}4pop DEF{..,F}5pop EFsame6pop F(empty)done

The trace runs bfs(graph, "A"). At each step the front vertex is popped and its unvisited neighbours are appended. Notice that D is discovered from B, so when C is processed, D is already in the visited set and is not enqueued twice. Marking a vertex visited when it is enqueued, not when it is popped, is what guarantees each vertex enters the queue at most once and keeps the algorithm at O(V + E). The output order is A, B, C, D, E, F.

7. Worked Example: Grid Shortest Path

A very common use of BFS is finding the minimum number of moves in a grid maze, where each open cell is a vertex and edges connect orthogonal neighbours. The graph is implicit: neighbours are computed on the fly instead of stored in an adjacency list.

from collections import deque

def grid_shortest(grid, start, goal):
    """grid: list of strings, '#' is a wall. Returns number of moves or -1."""
    rows, cols = len(grid), len(grid[0])
    dist = {start: 0}
    q = deque([start])
    while q:
        r, c = q.popleft()
        if (r, c) == goal:
            return dist[(r, c)]
        for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols \
               and grid[nr][nc] != '#' and (nr, nc) not in dist:
                dist[(nr, nc)] = dist[(r, c)] + 1
                q.append((nr, nc))
    return -1

maze = ["..#.",
        ".##.",
        "....",
        "#.#."]
print(grid_shortest(maze, (0, 0), (0, 3)))   # 7

The dist dictionary doubles as the visited set. The same pattern solves word ladders (vertices are words, edges differ by one letter), minimum knight moves, and social "degrees of separation" queries. For multiple sources (for example, distance from the nearest exit), seed the queue with all sources at distance 0: this is multi-source BFS.

8. Variants

9. Pitfalls

10. BFS vs DFS

PropertyBFSDFS
Frontier structureQueue (FIFO)Stack or recursion (LIFO)
Shortest path (unweighted)YesNo
MemoryO(width of graph)O(depth of graph)
Typical usesDistances, levels, nearest targetTopological sort, cycle detection, backtracking

Choose BFS when the answer depends on distance from the start; choose DFS when you need to explore whole branches or the graph is very wide.

← Back to Algorithms