Breadth-First Search (BFS)
Explore a graph layer by layer from a source using a FIFO queue, visiting every node at distance d before any node at distance d + 1.
Overview
BFS visits the nodes of a graph in order of their hop distance from a source: first the source, then all its neighbours, then all nodes two edges away, and so on. It is driven by a Queue: a node is dequeued, its unvisited neighbours are marked and enqueued, and the loop continues until the queue is empty.
Because it discovers nodes in non-decreasing distance order, BFS is the natural tool whenever "fewest steps" matters on an unweighted graph (see BFS Shortest Path (Unweighted)). It is also the traversal of choice for level-by-level processing such as Binary Tree level order, multi-source flood fills, and finding connected components without recursion.
Intuition
A mental model before the formal terms.
Drop a stone into still water: the ripple reaches every point at radius 1 before any point at radius 2. BFS is that ripple. The queue is the ring of the wavefront — everything in it is at distance d or d + 1, never further — and "visited" marks are the water already disturbed, so a ripple never re-enters the same spot.
How it works
- Mark the source visited and push it onto a queue.
- Pop the front node
u. For every neighbourvofuthat is not yet visited: markvvisited (at enqueue time, not at dequeue time), recordparent[v] = uanddist[v] = dist[u] + 1, and pushv. - Repeat until the queue is empty. Nodes are popped in non-decreasing
distorder; the order of first visits is the BFS order. - For a disconnected graph, wrap the whole thing in a loop over all nodes and start a new BFS from each unvisited node.
Why it works
Invariant: at any moment the queue holds nodes of at most two consecutive distances d, d, …, d, d+1, …, d+1 in that order. Popping a distance-d node can only enqueue distance-(d+1) nodes at the back, so the invariant is preserved.
From the invariant, the first time a node is discovered it is reached via a node at minimal distance, hence its dist is the true shortest hop count. Marking at enqueue time guarantees each node enters the queue exactly once, so each edge is scanned at most twice (once per endpoint in an undirected graph): O(V + E) total.
Recognition
How to tell a problem wants this.
- The problem says "shortest", "minimum number of moves/steps", "fewest edges" and every step costs the same.
- You need level-by-level output: "nodes at depth k", "level order", "rotting spreads one minute at a time".
- The state space is implicit (grid cells, word transformations, puzzle configurations) and you want the nearest goal state.
- Multiple sources spread simultaneously — start with all of them in the queue at distance 0.
Interactive visualization
Play, step, change the input. ← → and space work too.
1queue = [source]; visited = {source}2while queue not empty:3 u = queue.popleft()4 for v in neighbors(u):5 if v not in visited:6 visited.add(v); parent[v] = u7 queue.append(v)Pseudocode
1visited = {s}; queue = [s]; dist[s] = 02while queue not empty:3 u = queue.popleft()4 for v in adj[u]:5 if v not in visited:6 visited.add(v); dist[v] = dist[u] + 1; parent[v] = u7 queue.append(v)Implementations
1from collections import deque2 3 4def bfs(adj: list[list[int]], s: int) -> tuple[list[int], list[int]]:5 """adj[u] lists neighbours of u; nodes are 0..n-1.6 Returns (dist, parent) with dist = -1 for unreachable nodes."""71 · Initialize distances and queue8 n = len(adj)9 dist = [-1] * n10 parent = [-1] * n11 dist[s] = 012 q = deque([s])132 · Main loop14 while q:15 u = q.popleft()163 · Visit neighbours17 for v in adj[u]:18 if dist[v] == -1: # mark at enqueue time19 dist[v] = dist[u] + 120 parent[v] = u21 q.append(v)224 · Result23 return dist, parentcollections.dequegives O(1)popleft(); a plain list would be O(n) per pop.dist = [-1] * nis the visited set and the answer at the same time.- Neighbours are marked when appended, so each node is enqueued once.
- The function returns a tuple; unpack with
dist, parent = bfs(adj, s).
list.pop(0) would be O(n) per dequeue — always use deque.
dequeis the built-in queue;queue.Queueis a thread-safe primitive with locking overhead and is not meant for algorithms.while q:is the idiomatic emptiness check — a deque is falsy when empty.- For a
dict[int, list[int]]graph, iterateadj.get(u, [])so isolated nodes without an entry do not raiseKeyError.
- Using
list.pop(0)as the dequeue. - Using
if not dist[v]— the source has distance 0. - Forgetting
[-1] * ncreates a list of ints (fine) but[[]] * nwould alias the same inner list.
- Queue primitive: C++
std::queue(overstd::deque) and Pythoncollections.dequeare O(1) at both ends; JavaScript/TypeScript have no deque, so the code uses an array with a head index instead ofshift()(O(n)). - Visited encoding: all four use
dist == -1as "unvisited"; in JS/TS/Python beware truthiness —0is falsy, so never writeif (!dist[v]). - C++
queue::pop()returns void; you must readfront()first. Pythonpopleft()and the JS head index return the element directly.
Complexity
Queue plus visited set. On a grid with n cells, V = n and E ≤ 4n. With an adjacency matrix the time becomes O(V²).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Shortest path by edge count on an unweighted graph or grid.
- Level-by-level processing (tree level order, "spread one step per minute").
- Finding the nearest node satisfying a property — BFS can stop as soon as it pops one.
- Deep graphs where recursive Depth-First Search (DFS) would overflow the call stack; BFS is naturally iterative.
- Weighted edges: BFS ignores weights. Use Dijkstra's Algorithm (non-negative) or 0-1 BFS (weights 0/1).
- Problems that need finishing order, back-edge detection or path structure — Depth-First Search (DFS) gives those for free (cycle detection, Topological Sort, Bridges).
- Very wide graphs where memory is the bottleneck: the queue can hold an entire level, e.g. the whole frontier of a huge implicit state space. Iterative deepening DFS trades time for O(depth) memory.
Alternatives
Common mistakes
- Marking visited at dequeue time instead of enqueue time — nodes get pushed many times and the queue blows up to O(E).
- Using
Array.shift()in JavaScript as the queue: each shift is O(n), making BFS O(V²). Use a head index or a deque. - Forgetting to seed all sources for a multi-source BFS, or giving them distance 1 instead of 0.
- Not handling disconnected graphs when the task is "visit everything" — a single BFS only reaches one component.
- Re-computing neighbours of a grid cell with wrong bounds checks; validate
0 <= r < Rand0 <= c < Cbefore indexing.
Interview patterns
- Grid BFS with a 4- or 8-direction delta array (islands, rotting oranges, walls and gates).
- Multi-source BFS: push every source at distance 0 to compute "distance to nearest X" for all cells in one pass.
- BFS over implicit states: word ladder (words are nodes, one-letter changes are edges), lock combinations, sliding puzzles.
- Level-order tree traversal by processing
len(queue)nodes per iteration. - Bipartite check by 2-colouring nodes level by level (Bipartite Check).
- Course ScheduleIntermediate
- Network Delay TimeAdvanced
- Number of IslandsIntermediate