BFS explores a graph level by level from a start node, visiting all nearest nodes first before moving farther away.
- Find the shortest path in an unweighted graph.
- Traverse all nodes by distance from a source.
- Check connectivity (whether a node can be reached).
- Perform level-order traversal in trees.
- Start from a source node and mark it as visited.
- Put the source node into a queue.
- While the queue is not empty:
- Remove the front node.
- Process it (print/store/check target).
- For each unvisited neighbor, mark visited and add it to the queue.
- Stop when the queue is empty (or when your target is found).
Use when: visiting all reachable nodes or finding traversal order.
Input: graph — adjacency dict; start — starting node.
Output: list of visited nodes in BFS order.
from collections import deque
def bfs(graph, start):
visited = set([start])
order = []
queue = deque([start])
while queue:
# FIFO pop ensures level-by-level traversal.
node = queue.popleft()
order.append(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return orderUse when: multiple starting nodes all at distance 0 (e.g., 01-Matrix, rotting oranges).
Input: graph — adjacency dict; sources — list of starting nodes.
Output: dict mapping each reachable node to its distance from the nearest source.
from collections import deque
def multi_source_bfs(graph, sources):
dist = {}
queue = deque()
# All sources start at distance 0 at the same time.
for s in sources:
dist[s] = 0
queue.append(s)
while queue:
node = queue.popleft()
for nei in graph.get(node, []):
if nei not in dist:
dist[nei] = dist[node] + 1
queue.append(nei)
return distUse when: you need to process or record nodes grouped by their depth/level.
Input: graph — adjacency dict; start — starting node.
Output: list of lists, where each inner list contains nodes at one level.
from collections import deque
def bfs_levels(graph, start):
visited = {start}
queue = deque([start])
levels = []
while queue:
# Snapshot queue size to process exactly one level.
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node)
for nei in graph.get(node, []):
if nei not in visited:
visited.add(nei)
queue.append(nei)
levels.append(current_level)
return levelsUse when: finding shortest path between two specific nodes and the graph is large (reduces search space).
Input: graph — adjacency dict; start, target — endpoints.
Output: shortest path length, or -1 if unreachable.
from collections import deque
def bidirectional_bfs(graph, start, target):
if start == target:
return 0
front = {start}
back = {target}
visited = {start, target}
steps = 0
while front and back:
# Expand smaller frontier for better performance.
if len(front) > len(back):
front, back = back, front
next_front = set()
for node in front:
for nei in graph.get(node, []):
if nei in back:
return steps + 1
if nei not in visited:
visited.add(nei)
next_front.add(nei)
front = next_front
steps += 1
return -1- Time:
O(V + E)whereVis number of vertices andEis number of edges. - Space:
O(V)for the visited set and queue.
- Why do we use a queue in BFS instead of a stack?
- In what type of graph does BFS guarantee the shortest path?
- What can go wrong if we mark nodes as visited only when popping from the queue?
Answers
- Queue gives FIFO behavior, which processes nodes in level order.
- In an unweighted graph (or a graph where all edge weights are equal).
- The same node may be added to the queue multiple times, causing extra work.
At first, I mixed up BFS and DFS because both visit all nodes; BFS is the one that expands by levels using a queue.
- Depth-First Search (DFS)
- Dijkstra's Algorithm – BFS generalised to weighted graphs.
- Topological Sort – also uses a queue (Kahn's algorithm).