1Breadth-First Search (BFS)
▶
Breadth-First Search (BFS) is one of the most fundamental graph traversal algorithms in computer science. At its core, BFS answers a deceptively simple question: starting from a given node, in what order should we visit every other reachable node if we want to always explore closer nodes before farther ones? The answer is a systematic, layer-by-layer expansion outward from the source — much like the ripples that spread across a pond when a stone is dropped in. BFS is applicable to both directed graphs (where edges have a direction, like one-way streets) and undirected graphs (where edges go both ways, like hallways in a building), making it an extremely versatile tool.
To understand why BFS behaves the way it does, it helps to think of the graph as a series of concentric "shells" centered on the starting node. The source node itself forms shell zero. All nodes directly connected to the source form shell one (distance 1). All nodes reachable in exactly two hops — but not reachable in one — form shell two (distance 2), and so on. BFS visits every node in shell one before touching any node in shell two, visits every node in shell two before touching shell three, and continues in this fashion until no more reachable nodes remain. This guarantee is not just a nice property — it is the mathematical foundation for BFS's ability to find shortest paths.
The Queue Data Structure in BFS
The key to BFS's level-by-level behavior is its use of a queue — a First-In, First-Out (FIFO) data structure. A queue is analogous to a line of people waiting: the person who joined first is the first to be served, and new arrivals join at the back. In BFS, nodes play the role of people in the line.
- Enqueuing (adding to the back): When a node is first discovered — meaning we see it as a neighbor of the current node and it has not been visited before — it is immediately marked as visited and placed at the back of the queue. Marking it as visited at the moment of enqueuing (rather than at the moment of processing) is crucial: it prevents the same node from being added to the queue multiple times if it appears as a neighbor of several already-processed nodes.
- Dequeuing (removing from the front): At each step of the algorithm, the node at the front of the queue is removed and processed. Because earlier-discovered nodes sit closer to the front, they are always processed before later-discovered nodes. This is precisely what enforces the level-by-level order.
- The visited set: Alongside the queue, BFS maintains a visited set (or a boolean array indexed by node identifier). Before enqueuing any neighbor, the algorithm checks whether that neighbor is already in the visited set. If it is, the neighbor is skipped entirely. This prevents infinite loops in graphs with cycles and keeps the time complexity manageable.
Consider a small example to see the queue in action. Suppose we have the following undirected graph:
A
/ \
B C
/ \ \
D E F
If we start BFS at node A, the queue and visited set evolve as follows:
| Step | Action | Queue (front → back) | Visited |
|---|---|---|---|
| 0 | Initialize: mark A visited, enqueue A | [A] | {A} |
| 1 | Dequeue A; enqueue unvisited neighbors B, C | [B, C] | {A, B, C} |
| 2 | Dequeue B; enqueue unvisited neighbors D, E | [C, D, E] | {A, B, C, D, E} |
| 3 | Dequeue C; enqueue unvisited neighbor F | [D, E, F] | {A, B, C, D, E, F} |
| 4 | Dequeue D; no unvisited neighbors | [E, F] | {A, B, C, D, E, F} |
| 5 | Dequeue E; no unvisited neighbors | [F] | {A, B, C, D, E, F} |
| 6 | Dequeue F; no unvisited neighbors. Queue empty — done. | [] | {A, B, C, D, E, F} |
The traversal order is A → B → C → D → E → F, which perfectly mirrors the layer structure: A (distance 0), then B and C (distance 1), then D, E, and F (distance 2).
BFS Algorithm Logic Step by Step
The BFS algorithm can be stated precisely in four steps that repeat until completion:
- Step 1 — Initialization: Mark the source node as visited and add it to the queue. This bootstraps the process. At this point the queue contains exactly one element.
- Step 2 — Dequeue and examine: Remove the node at the front of the queue. This node is the current node being "processed." Examine each of its neighbors one by one.
- Step 3 — Discover new neighbors: For each neighbor of the current node, check whether it has already been visited. If it has not been visited, mark it as visited immediately and enqueue it at the back of the queue. If it has already been visited, skip it.
- Step 4 — Repeat or terminate: Return to Step 2. If the queue is empty at the start of Step 2, the algorithm terminates. At this point, every node reachable from the source has been visited exactly once.
Here is a concrete Python-style pseudocode implementation of BFS:
from collections import deque
def bfs(graph, source):
visited = set()
queue = deque()
# Step 1: Initialize
visited.add(source)
queue.append(source)
while queue: # Step 4: repeat until queue is empty
node = queue.popleft() # Step 2: dequeue front node
print(node) # process the node (e.g., print it)
for neighbor in graph[node]: # Step 3: examine each neighbor
if neighbor not in visited:
visited.add(neighbor) # mark visited before enqueuing
queue.append(neighbor) # enqueue for later processing
In terms of time complexity, BFS visits every node once and examines every edge once (or twice in an undirected graph, once from each endpoint). This gives a time complexity of O(V + E), where V is the number of vertices and E is the number of edges. Space complexity is also O(V) in the worst case, because the queue and visited set together can hold at most all vertices.
BFS Traversal Order
One of BFS's most important properties is the order in which it visits nodes. Because the queue enforces FIFO processing, all nodes at distance k from the source are fully dequeued and processed before any node at distance k + 1 is dequeued. This means:
- All nodes at distance 1 (the direct neighbors of the source) are visited before any node at distance 2. All nodes at distance 2 are visited before any node at distance 3, and so on.
- The sequence in which BFS visits nodes is a record of the shortest hop count from the source. If you track the order of first visits, you can reconstruct the shortest path (in terms of edge count) from the source to any reachable node.
- Among nodes at the same distance from the source, the relative order of visitation is determined by the order in which their "parent" nodes were enqueued and by the order neighbors appear in the adjacency list. This detail matters for reproducibility and for problems that require a specific tie-breaking rule, but it does not affect the correctness of the shortest-path guarantee.
To illustrate the distance guarantee, consider recording the distance of each node from the source by extending BFS slightly:
def bfs_with_distances(graph, source):
visited = set()
queue = deque()
distance = {}
visited.add(source)
queue.append(source)
distance[source] = 0
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
distance[neighbor] = distance[node] + 1
queue.append(neighbor)
return distance
After this runs, distance[v] holds the minimum number of edges needed to reach node v from the source. No other traversal method guarantees this without additional bookkeeping — notably, Depth-First Search (DFS) does not provide this guarantee.
Handling Disconnected Graphs in BFS
A disconnected graph is one where not every pair of nodes is linked by a path. For example, a social network might have isolated clusters of friends who have no mutual connections with other clusters. When BFS is started from a single source node, it can only reach nodes that are in the same connected component as the source. Nodes in other components will never be enqueued, so they remain unvisited when the queue empties.
To perform a complete traversal of a disconnected graph — visiting every node in every component — the standard technique is to wrap BFS in an outer loop that iterates over all nodes:
def bfs_all_components(graph):
visited = set()
for start_node in graph:
if start_node not in visited:
# start_node is in a previously unseen component
bfs_component(graph, start_node, visited)
def bfs_component(graph, source, visited):
queue = deque()
visited.add(source)
queue.append(source)
while queue:
node = queue.popleft()
print(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
Each time the outer loop finds a node that has not yet been visited, it knows that node belongs to a new, previously undiscovered component, and it launches a fresh BFS from there. The visited set is shared across all BFS calls so that nodes already explored in earlier component traversals are not re-processed. This approach discovers all connected components of the graph and visits every node exactly once overall, preserving the O(V + E) total complexity.
Practical Applications of BFS
BFS is far more than a textbook exercise. Its level-by-level exploration and shortest-path guarantee make it the algorithm of choice for a wide range of real-world problems:
- Shortest path in unweighted graphs: This is BFS's signature application. In any graph where all edges have equal (or no) weight, BFS from a source node computes the minimum number of hops to every other reachable node. Navigation apps, for example, model a city's street network as a graph and use variants of BFS to find routes with the fewest turns or stops.
- Degrees of separation in social networks: Platforms like LinkedIn and Facebook model users as nodes and friendships or connections as edges. BFS can answer questions like "How many connections separate person A from person B?" — the famous "six degrees of separation" concept. The algorithm discovers all of A's direct connections first (1st degree), then their connections (2nd degree), and so on, stopping when it finds B.
- Web crawlers: A web crawler starts from a seed URL (the source node) and explores the web by following hyperlinks (edges). BFS-based crawlers process all links on the current page before following links on newly discovered pages, which keeps the crawl geographically and topically close to the seed in early stages. This is useful for building localized search indexes.
- Peer-to-peer networks: In peer-to-peer file-sharing systems (like BitTorrent trackers or distributed hash tables), BFS is used to discover nearby peers in the network topology, minimizing latency by preferring nodes that are fewer hops away.
- GPS and mapping navigation: Road networks, transit maps, and flight path graphs can all be traversed with BFS to find routes with the minimum number of segments or transfers, even before considering travel time or distance (which would require weighted shortest-path algorithms like Dijkstra's).
- Puzzle solving: BFS is the natural choice for puzzles that ask for the minimum number of moves to reach a goal state. A classic example is the sliding-tile (15-puzzle): each puzzle configuration is a node, and each valid tile slide is an edge. BFS finds the solution with the fewest moves because it explores all 1-move solutions before any 2-move solution, all 2-move solutions before any 3-move solution, and so forth. The same principle applies to Rubik's Cube solvers (for small subproblems), word-ladder puzzles, and many others.
- Cycle detection and bipartiteness testing: BFS can determine whether an undirected graph is bipartite (can be 2-colored such that no two adjacent nodes share the same color) by attempting to color nodes in alternating layers. If a conflict is found, the graph is not bipartite and contains an odd-length cycle.
In summary, Breadth-First Search is a powerful, elegant algorithm whose correctness rests on a single structural insight: a queue processes nodes in the order they are discovered, and nodes closer to the source are always discovered first. This simple guarantee enables BFS to solve a remarkable variety of problems efficiently, making it an indispensable tool in any programmer's or computer scientist's toolkit.