Depth First Search explores a graph by traversing as deep as possible along each branch before backtracking. It's fundamental for pathfinding, cycle detection, topological sorting, and connected component analysis.
Graph Representation
DFS typically operates on an adjacency list:
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}Recursive Implementation
The recursive approach uses the call stack implicitly:
def dfs_recursive(graph, node, visited=None):
if visited is None:
visited = set()
visited.add(node)
print(node, end=' ')
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
return visited
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
visited = dfs_recursive(graph, 'A')Output:
A B D E F CWith return path tracking:
def dfs_with_path(graph, start, target, path=None, visited=None):
"""Find a path from start to target using DFS."""
if path is None:
path = []
if visited is None:
visited = set()
path = path + [start]
visited.add(start)
if start == target:
return path
for neighbor in graph[start]:
if neighbor not in visited:
result = dfs_with_path(graph, neighbor, target, path, visited)
if result:
return result
return None
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
path = dfs_with_path(graph, 'A', 'F')
print(path)Output:
['A', 'B', 'E', 'F']Iterative Implementation
Using an explicit stack prevents recursion limit issues:
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)
for neighbor in reversed(graph[node]):
if neighbor not in visited:
stack.append(neighbor)
return result
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
order = dfs_iterative(graph, 'A')
print(order)Output:
['A', 'B', 'D', 'E', 'F', 'C']Finding All Paths
Discover all possible paths between two nodes:
def find_all_paths(graph, start, end, path=None):
"""Find all paths from start to end."""
if path is None:
path = []
path = path + [start]
if start == end:
return [path]
if start not in graph:
return []
paths = []
for neighbor in graph[start]:
if neighbor not in path: # Avoid cycles
new_paths = find_all_paths(graph, neighbor, end, path)
paths.extend(new_paths)
return paths
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
all_paths = find_all_paths(graph, 'A', 'F')
for p in all_paths:
print(' -> '.join(p))Output:
A -> B -> E -> F
A -> C -> FCycle Detection
Detect cycles in a directed graph:
def has_cycle(graph):
"""Detect if directed graph has a cycle using DFS."""
WHITE, GRAY, BLACK = 0, 1, 2
color = {node: WHITE for node in graph}
def dfs(node):
color[node] = GRAY # Currently being processed
for neighbor in graph[node]:
if color[neighbor] == GRAY: # Back edge found
return True
if color[neighbor] == WHITE:
if dfs(neighbor):
return True
color[node] = BLACK # Fully processed
return False
for node in graph:
if color[node] == WHITE:
if dfs(node):
return True
return False
cyclic_graph = {
'A': ['B'],
'B': ['C'],
'C': ['A'] # Creates cycle
}
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
print(has_cycle(cyclic_graph)) # True
print(has_cycle(graph)) # FalseTopological Sort
Order nodes so all edges point forward:
def topological_sort(graph):
"""Return topologically sorted order of nodes."""
visited = set()
result = []
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
result.append(node)
for node in graph:
if node not in visited:
dfs(node)
return result[::-1] # Reverse for correct order
tasks = {
'build': ['test'],
'test': ['deploy'],
'deploy': [],
'lint': ['build'],
'install': ['lint', 'build']
}
order = topological_sort(tasks)
print(order) # ['install', 'lint', 'build', 'test', 'deploy']Connected Components
Find all connected components in an undirected graph:
def find_connected_components(graph):
"""Find all connected components."""
visited = set()
components = []
def dfs(node, component):
visited.add(node)
component.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor, component)
for node in graph:
if node not in visited:
component = []
dfs(node, component)
components.append(component)
return components
disconnected = {
'A': ['B'],
'B': ['A'],
'C': ['D'],
'D': ['C'],
'E': []
}
components = find_connected_components(disconnected)
print(components) # [['A', 'B'], ['C', 'D'], ['E']]DFS on Grid/Matrix
Navigate a 2D grid:
def dfs_grid(grid, start_row, start_col):
"""DFS on a 2D grid."""
rows, cols = len(grid), len(grid[0])
visited = set()
def dfs(r, c):
if (r < 0 or r >= rows or
c < 0 or c >= cols or
(r, c) in visited or
grid[r][c] == 0): # 0 = blocked
return
visited.add((r, c))
print(f"Visiting: ({r}, {c})")
dfs(r + 1, c) # Down
dfs(r - 1, c) # Up
dfs(r, c + 1) # Right
dfs(r, c - 1) # Left
dfs(start_row, start_col)
return visited
grid = [
[1, 1, 0, 1],
[1, 1, 1, 0],
[0, 1, 1, 1]
]
visited = dfs_grid(grid, 0, 0)Output:
Visiting: (0, 0)
Visiting: (1, 0)
Visiting: (1, 1)
Visiting: (2, 1)
Visiting: (2, 2)
Visiting: (1, 2)
Visiting: (2, 3)
Visiting: (0, 1)Pre-order and Post-order Traversal
Track entry and exit times:
def dfs_with_timestamps(graph, start):
"""DFS with pre-order and post-order timestamps."""
visited = set()
time = [0]
pre_order = {}
post_order = {}
def dfs(node):
visited.add(node)
time[0] += 1
pre_order[node] = time[0]
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
time[0] += 1
post_order[node] = time[0]
dfs(start)
return pre_order, post_order
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
pre, post = dfs_with_timestamps(graph, 'A')
print("Pre-order:", pre)
print("Post-order:", post)Output:
Pre-order: {'A': 1, 'B': 2, 'D': 3, 'E': 5, 'F': 6, 'C': 10}
Post-order: {'D': 4, 'F': 7, 'E': 8, 'B': 9, 'C': 11, 'A': 12}Implementation Comparison
| Feature | Recursive | Iterative |
|---|---|---|
| Memory | Call stack | Explicit stack |
| Depth limit | ~1000 (default) | Limited by RAM |
| Code complexity | Simpler | More verbose |
| Debugging | Stack traces | Manual tracking |
| Performance | Slightly slower | Slightly faster |
Use the recursive implementation for readable code and moderate graph sizes. Switch to iterative for production systems handling deep or potentially cyclic graphs where stack overflow is a concern.