🌲 BFS & DFS Fundamentals
🎯 What are BFS and DFS?
BFS (Breadth-First Search) and DFS (Depth-First Search) are the two fundamental graph traversal algorithms. BFS explores neighbors level by level using a queue, while DFS explores as far as possible along each branch before backtracking using a stack (or recursion).
Core Concepts
BFS: "Wave propagation" - finds shortest path in unweighted graphs. Uses a queue (FIFO).
DFS: "Maze exploration" - uses stack (LIFO) or recursion. Great for topological sorting and cycle detection.
from collections import deque
# BFS - Queue based
def bfs(graph, start):
visited = set()
queue = deque([start])
visited.add(start)
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return result
# DFS - Recursive
def dfs_recursive(graph, node, visited=None):
if visited is None:
visited = set()
visited.add(node)
result = [node]
for neighbor in graph[node]:
if neighbor not in visited:
result.extend(dfs_recursive(graph, neighbor, visited))
return result
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print("BFS:", bfs(graph, 'A')) # ['A','B','C','D','E','F']
print("DFS:", dfs_recursive(graph, 'A')) # ['A','B','D','E','F','C']🔵 BFS - Breadth First Search
🎯 How BFS Works
BFS explores vertices in order of their distance from the source. It uses a queue to process nodes level by level. This guarantees the shortest path (minimum number of edges) to every reachable node in an unweighted graph.
- Data Structure: Queue (FIFO)
- Level-order traversal: All nodes at distance k are processed before nodes at distance k+1
- Shortest Path: First time a node is visited is via the shortest path
# BFS with shortest path reconstruction
def bfs_shortest_path(graph, start, target):
visited = {start}
queue = deque([(start, [start])])
while queue:
node, path = queue.popleft()
if node == target:
return path
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None🟣 DFS - Depth First Search
🎯 How DFS Works
DFS explores as far as possible along each branch before backtracking. It uses a stack (either explicitly or via recursion). DFS is memory efficient for deep graphs and useful for detecting cycles, topological sorting, and solving puzzles.
- Data Structure: Stack (LIFO) or recursion
- Backtracking: Natural for exploring all possibilities
- Pre/Post-order: Can process nodes on entry and exit
# DFS - Iterative using explicit stack
def dfs_iterative(graph, start):
visited = set()
stack = [start]
result = []
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
result.append(node)
# Add neighbors in reverse order for natural order
for neighbor in reversed(graph[node]):
if neighbor not in visited:
stack.append(neighbor)
return result⏱️ Time & Space Complexity
🎯 Complexity Analysis
- Time Complexity (Both): O(V + E) using adjacency list — each vertex and edge processed once.
- Space Complexity (BFS): O(V) for queue + visited set. Worst-case queue size = V.
- Space Complexity (DFS Recursive): O(V) recursion stack (worst-case depth = V).
- Space Complexity (DFS Iterative): O(V) for explicit stack.
⚖️ BFS vs DFS Comparison
- BFS Shortest Path: Yes (unweighted graphs) | DFS No
- BFS Memory: More (stores entire level) | DFS Less (only current path)
- BFS Completeness: Always finds solution if exists | DFS May get stuck in infinite loops
- BFS Use Cases: Shortest path, web crawling, social networks | DFS Topological sort, cycle detection, maze solving
💻 Multiple Implementations
# BFS on Grid (2D)
def bfs_grid(grid, start, target):
rows, cols = len(grid), len(grid[0])
directions = [(0,1),(1,0),(0,-1),(-1,0)]
queue = deque([(start[0], start[1], 0)]) # (r,c,distance)
visited = set()
visited.add(start)
while queue:
r, c, dist = queue.popleft()
if (r, c) == target:
return dist
for dr, dc in directions:
nr, nc = r+dr, c+dc
if 0 <= nr < rows and 0 <= nc < cols and (nr,nc) not in visited and grid[nr][nc] != '#':
visited.add((nr,nc))
queue.append((nr,nc,dist+1))
return -1
# DFS for connected components
def connected_components(graph):
visited = set()
components = []
for node in graph:
if node not in visited:
component = dfs_recursive(graph, node, set())
visited.update(component)
components.append(component)
return components📊 Step-by-Step Visualization
🎯 Graph: A connected to B, C; B to D, E; C to F
BFS from A: Queue = [A] → process A → queue=[B,C] → process B → queue=[C,D,E] → process C → queue=[D,E,F] → process D/E/F → done.
DFS from A: Stack = [A] → pop A → push C,B (order matters) → pop B → push E,D → pop D → ... gives A,B,D,E,C,F or similar.
🎯 Applications of BFS & DFS
- BFS: GPS navigation, social network friend suggestions, web crawling, peer-to-peer networks, minimum spanning tree (Prim's)
- DFS: Solving puzzles/mazes, topological sorting, detecting cycles, finding strongly connected components (Kosaraju/Tarjan), generating permutations/combinations
🎯 Directed vs Undirected Graphs
Both BFS and DFS work on directed and undirected graphs. For undirected graphs, simply ensure you don't revisit the parent. For directed graphs, edges have direction — traversal only follows edge direction. Cycle detection in directed graphs requires tracking nodes in the current recursion stack (DFS).
# Cycle detection in directed graph using DFS
def has_cycle_directed(graph):
visited = set()
rec_stack = set()
def dfs(node):
visited.add(node)
rec_stack.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
if dfs(neighbor):
return True
elif neighbor in rec_stack:
return True
rec_stack.remove(node)
return False
for node in graph:
if node not in visited:
if dfs(node):
return True
return False🔄 Iterative vs Recursive DFS
- Recursive DFS: Cleaner code, but risk of stack overflow for deep graphs (>1000 depth).
- Iterative DFS: Uses explicit stack, no recursion limit issues, slightly more verbose.
- Performance: Iterative often faster (no function call overhead).
🎨 Graph Coloring & Detection
🎯 Bipartite Graph Check (BFS/DFS)
A graph is bipartite if vertices can be colored with 2 colors such that no adjacent vertices share the same color. BFS works perfectly: start with one color, assign opposite to neighbors. If conflict found → not bipartite.
def is_bipartite(graph):
color = {}
for node in graph:
if node not in color:
queue = deque([node])
color[node] = 0
while queue:
curr = queue.popleft()
for neighbor in graph[curr]:
if neighbor not in color:
color[neighbor] = 1 - color[curr]
queue.append(neighbor)
elif color[neighbor] == color[curr]:
return False
return True🔄 Bidirectional BFS
🎯 Two-Ended Search for Speed
For large graphs where both source and target are known, run BFS from both ends simultaneously. When frontiers meet, the path is found. Time complexity reduces from O(b^d) to O(b^(d/2)) — massive improvement for large search spaces.
🚀 Parallel BFS/DFS
🎯 Concurrent Graph Traversal
BFS level-by-level can be parallelized by processing all nodes at current level simultaneously. GPU implementations can achieve significant speedups. DFS is harder to parallelize due to its sequential nature but can be done with work-stealing.
💪 Practice & Problem Patterns
🎯 Classic BFS/DFS Problems
- Number of Islands (LeetCode 200): DFS/BFS to count connected components.
- Word Ladder (LeetCode 127): BFS for shortest transformation sequence.
- Course Schedule (LeetCode 207): Cycle detection with DFS.
- Pacific Atlantic Water Flow: Multi-source BFS/DFS.
- Surrounded Regions: BFS/DFS from boundaries.
- Binary Tree Level Order Traversal: BFS on trees.
# LeetCode 200 - Number of Islands
def numIslands(grid):
if not grid: return 0
rows, cols = len(grid), len(grid[0])
visited = set()
def dfs(r, c):
if (r,c) in visited or r<0 or r>=rows or c<0 or c>=cols or grid[r][c]=='0':
return
visited.add((r,c))
for dr,dc in [(0,1),(1,0),(0,-1),(-1,0)]:
dfs(r+dr, c+dc)
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1' and (r,c) not in visited:
dfs(r,c)
count += 1
return count