Breadth-First Search
Difficulty: Easy. ROI: High.
Other common names: BFS, level-order traversal (on trees).
When to use: Use when you need shortest path in an unweighted graph, or to process a tree/graph level by level (e.g. “minimum steps to reach target,” level-order traversal).
Idea: Use a queue. Start with the source in the queue. Repeatedly dequeue a node, process it, and enqueue its unvisited neighbors. Ensures you visit nodes in order of increasing distance from the source.
Time / space: O(V + E) for graph with V vertices and E edges; O(n) for a tree with n nodes. Space O(n) for the queue.
Example #1 — Shortest Path in Unweighted Graph
Section titled “Example #1 — Shortest Path in Unweighted Graph”Find the minimum number of edges from start to end by exploring level by level with a queue. First time we reach a node, we have the shortest path to it.
Sample input and output
graph = {1: [2, 3], 2: [1, 4], 3: [1], 4: [2]}print(bfs_shortest_path(graph, 1, 4)) # 2 (path 1 → 2 → 4)print(bfs_shortest_path(graph, 1, 1)) # 0- Sample input:
graph = {1: [2, 3], 2: [1, 4], 3: [1], 4: [2]}, start 1, end 4. - Sample output:
2— shortest path from 1 to 4 has 2 edges (1 → 2 → 4).
Code (with inline comments)
from collections import deque
def bfs_shortest_path(graph, start, end): queue = deque([(start, 0)]) # (node, distance); FIFO so we explore by level seen = {start} while queue: node, dist = queue.popleft() # take next node (smallest distance so far) if node == end: return dist for neighbor in graph[node]: if neighbor not in seen: seen.add(neighbor) queue.append((neighbor, dist + 1)) # one step further return -1Line-by-line:
from collections import deque— A deque (double-ended queue) supports fast append and pop from both ends. We use it as a FIFO queue: we add neighbors at the back and always take from the front, so we process nodes in order of increasing distance.queue = deque([(start, 0)])— We start with the start node and distance 0. The queue holds pairs(node, distance_from_start).seen = {start}— A set of nodes we’ve already added to the queue (or processed). We skip a neighbor if it’s already inseen, so we don’t revisit nodes and the first time we see a node we have the shortest path to it.while queue:— Keep processing until the queue is empty (then we’ve reached everything reachable and didn’t findend).node, dist = queue.popleft()— Take the next node from the front of the queue. Because we always push nodes with distance dist+1, we process nodes in order of distance (level by level).if node == end: return dist— We reached the target; the distance we stored is the shortest path length.for neighbor in graph[node]:— Look at every neighbor of the current node.graph[node]is the list of nodes connected tonode(adjacency list).if neighbor not in seen:— Only add a neighbor we haven’t seen yet; that way we never overwrite a shorter distance.seen.add(neighbor); queue.append((neighbor, dist + 1))— Mark as seen and add to the back of the queue with distance one more than the current node.
Execution trace (numbered list)
For start=1, end=4:
- Step 0: queue=[(1,0)], seen =
{1}. - Step 1: Pop (1,0). 1≠4. Add 2, 3 → queue=[(2,1),(3,1)], seen =
{1, 2, 3}. - Step 2: Pop (2,1). 2≠4. Add 4 → queue=[(3,1),(4,2)], seen =
{1, 2, 3, 4}. - Step 3: Pop (3,1). 3≠4; 4 already in seen, skip. queue=[(4,2)].
- Step 4: Pop (4,2). 4==4 → return 2.
Execution trace (code with step comments)
# Step 0: queue=[(1,0)], seen={1}# Step 1: popleft (1,0), add (2,1),(3,1) → queue=[(2,1),(3,1)], seen={1,2,3}# Step 2: popleft (2,1), add (4,2) → queue=[(3,1),(4,2)], seen={1,2,3,4}# Step 3: popleft (3,1), 4 in seen → skip# Step 4: popleft (4,2), node==end → return 2Execution trace (ASCII diagram)
Levels from start; we process by distance and return when we hit end.
Graph: 1 --- 2 --- 4 | 3
Level 0: [1]Level 1: [2, 3]Level 2: [4] → first time we see 4, return dist=2