21. BFS & DFS — Graph Traversals
876 words
4 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 21. BFS & DFS — Graph Traversals > **What problem does this solve?** You have a graph.

21. BFS & DFS — Graph Traversals
What problem does this solve? You have a graph. How do you visit every reachable vertex? Two fundamental strategies exist: BFS (breadth-first, level by level, using a queue) and DFS (depth-first, go as deep as possible, using a stack/recursion).
1. Breadth-First Search (BFS)
Mental Model
BFS is like dropping a stone in a pond — the ripples spread outward one layer at a time. It finds the shortest path in unweighted graphs.
(Diagram)
Implementation
python# runnable from collections import deque def bfs(graph, start): """BFS traversal. Returns order of visitation. Time: O(V + E) Space: O(V) for queue + visited set """ visited = {start} queue = deque([start]) order = [] while queue: vertex = queue.popleft() order.append(vertex) for neighbor in graph[vertex]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return order # Graph as adjacency list graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } print(f"BFS: {bfs(graph, 'A')}") # ['A', 'B', 'C', 'D', 'E', 'F']
BFS Shortest Path
python# runnable def bfs_shortest_path(graph, start, target): """Return shortest path from start to target using BFS.""" visited = {start} queue = deque([(start, [start])]) # (vertex, path so far) while queue: vertex, path = queue.popleft() if vertex == target: return path for neighbor in graph[vertex]: if neighbor not in visited: visited.add(neighbor) queue.append((neighbor, path + [neighbor])) return None # No path print(f"Shortest path A→F: {bfs_shortest_path(graph, 'A', 'F')}") # ['A', 'C', 'F']
2. Depth-First Search (DFS)
Mental Model
DFS is like exploring a maze — you go down one path until you hit a dead end, then backtrack and try another path. It's naturally recursive.
(Diagram)
Implementation (Recursive)
python# runnable def dfs_recursive(graph, start): """DFS traversal using recursion.""" visited = set() order = [] def _dfs(v): visited.add(v) order.append(v) for neighbor in graph[v]: if neighbor not in visited: _dfs(neighbor) _dfs(start) return order print(f"DFS rec: {dfs_recursive(graph, 'A')}") # ['A', 'B', 'D', 'E', 'F', 'C']
Implementation (Iterative with Stack)
python# runnable def dfs_iterative(graph, start): """DFS traversal using explicit stack.""" visited = set() stack = [start] order = [] while stack: vertex = stack.pop() if vertex not in visited: visited.add(vertex) order.append(vertex) # Add in reverse order for consistent traversal for neighbor in reversed(graph[vertex]): if neighbor not in visited: stack.append(neighbor) return order print(f"DFS iter: {dfs_iterative(graph, 'A')}")
3. Preorder and Postorder Numbers (DFS)
DFS can assign preorder (when first discovered) and postorder (when all descendants processed) numbers to each vertex. This is crucial for topological sorting and cycle detection.
python# runnable def dfs_prepost(graph): """Assign preorder and postorder numbers to each vertex.""" visited = set() pre = {} post = {} clock = [0] # Mutable counter def _dfs(v): visited.add(v) clock[0] += 1 pre[v] = clock[0] for neighbor in graph[v]: if neighbor not in visited: _dfs(neighbor) clock[0] += 1 post[v] = clock[0] for v in graph: if v not in visited: _dfs(v) return pre, post pre, post = dfs_prepost(graph) print(f"Pre: {pre}") # {'A': 1, 'B': 2, 'D': 3, 'E': 4, 'F': 5, 'C': 6} print(f"Post: {post}") # {'D': 4, 'F': 7, 'E': 8, 'B': 9, 'C': 10, 'A': 12}
4. Connected Components
python# runnable def connected_components(graph): """Find all connected components in an undirected graph.""" visited = set() components = [] for v in graph: if v not in visited: # BFS/DFS to find all vertices in this component component = [] queue = deque([v]) visited.add(v) while queue: curr = queue.popleft() component.append(curr) for neighbor in graph[curr]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) components.append(component) return components # Disconnected graph disc = { 'A': ['B'], 'B': ['A'], 'C': ['D'], 'D': ['C'], 'E': [] } print(f"Components: {connected_components(disc)}") # ['A', 'B'], ['C', 'D'], ['E'](/courses/bscs2002/notes/'A'%2C%20'B'%5D%2C%20%5B'C'%2C%20'D'%5D%2C%20%5B'E')
5. Applications
| Algorithm | Use Case |
|---|---|
| BFS | Shortest path (unweighted), web crawling, social networks |
| DFS | Topological sort, cycle detection, connected components, solving mazes |
| BFS + Queue | Finding shortest path in unweighted graphs |
| DFS + Pre/Post | Detecting cycles, topological ordering |
Practice Questions
Q1. Trace BFS on a graph A→B→D, A→C→E. What's the visitation order?
Q2. When would you use BFS over DFS, and vice versa?
Q3. In DFS iterative, why do we add neighbors in reverse order?
Q4. How can you detect a cycle in a directed graph using DFS?
Q5. What's the time complexity of BFS/DFS? Why is it O(V+E) and not O(V×E)?
AnswersA1. BFS: A, B, C, D, E (level by level).A2. BFS: shortest path, any problem where closer neighbors are better. DFS: topological sort, cycle detection, maze solving, exhaustive search.A3. To maintain the same traversal order as the recursive version (which processes neighbors in list order).A4. During DFS, if we encounter a back edge (an edge to an ancestor in the DFS tree — a vertex currently on the recursion stack), there's a cycle.A5. Each vertex is visited once (V visits). For each vertex, we examine all its incident edges. Sum of degrees = 2E for undirected, E for directed. Total: O(V + E). Join Discord Previous20. Graph Representations — Adjacency Matrix & ListNext22. Topological Sort & DAG Longest Path