Master Depth-First Search completely — recursive and iterative implementations, DFS tree and timestamps, cycle detection in directed and undirected graphs, finding articulation points and bridges, DFS on grids, topological sort foundation, and all classic DFS interview problems.
What Is DFS?
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It uses a stack (implicitly via recursion, or explicitly) to track the path.
Intuition: Navigating a maze — always take the first available path, go as deep as possible, and only backtrack when you hit a dead end.
| Graph | DFS from vertex 0: |
| 0—1—3 Explore 0 | go deep through 1 → go deep through 3 |
| | Backtrack | try 2 from 0 |
| 2 Backtrack | done |
| DFS order: 0 | 1 → 3 → 2 (depth-first, not level-by-level) |
| BFS order: 0 | 1 → 2 → 3 (breadth-first, level-by-level) |
Iterative DFS (Explicit Stack)
def dfs_iterative(graph, start):
"""
Iterative DFS using explicit stack.
Note: order may differ from recursive due to stack reversal.
"""
visited = set()
stack = [start]
order = []
while stack:
vertex = stack.pop() # LIFO: take from top
if vertex in visited:
continue # Skip already-processed vertices
visited.add(vertex)
order.append(vertex)
# Add neighbors in reverse order for consistent left-to-right traversal
for neighbor in reversed(graph[vertex]):
if neighbor not in visited:
stack.append(neighbor)
return order
DFS Timestamps
Timestamps give each vertex two values:
- Discovery time (d[v]): When vertex was first visited
- Finish time (f[v]): When all neighbors were explored (vertex "closed")
def dfs_with_timestamps(graph):
"""
DFS with discovery and finish timestamps.
Used for: cycle detection, topological sort, SCC detection.
"""
n = len(graph)
color = {v: 'WHITE' for v in graph} # WHITE=unvisited, GRAY=in progress, BLACK=done
d = {} # Discovery time
f = {} # Finish time
time = [0]
def dfs_visit(u):
color[u] = 'GRAY' # Discovered
time[0] += 1
d[u] = time[0] # Record discovery time
for v in graph[u]:
if color[v] == 'WHITE':
dfs_visit(v)
elif color[v] == 'GRAY':
print(f"Back edge {u}→{v}: cycle detected!")
color[u] = 'BLACK' # Finished
time[0] += 1
f[u] = time[0] # Record finish time
for u in graph:
if color[u] == 'WHITE':
dfs_visit(u)
return d, f
Cycle Detection
Undirected Graph — Any Back Edge = Cycle
def has_cycle_undirected(graph):
"""
Detect cycle in undirected graph using DFS.
A cycle exists if we encounter an already-visited vertex
that is not the parent of the current vertex.
"""
visited = set()
def dfs(v, parent):
visited.add(v)
for neighbor in graph[v]:
if neighbor not in visited:
if dfs(neighbor, v):
return True
elif neighbor != parent:
# Found visited vertex that is NOT our parent → back edge → cycle
return True
return False
# Check all components
for v in graph:
if v not in visited:
if dfs(v, -1):
return True
return False
g1 = {0:[1,2], 1:[0,2], 2:[0,1]} # Triangle — has cycle
g2 = {0:[1], 1:[0,2], 2:[1]} # Path — no cycle
print(has_cycle_undirected(g1)) # True
print(has_cycle_undirected(g2)) # False
Directed Graph — Gray (In-Progress) Vertices = Cycle
def has_cycle_directed(graph):
"""
Detect cycle in directed graph.
A cycle exists if we find an edge to a GRAY (in-progress) vertex.
"""
WHITE, GRAY, BLACK = 0, 1, 2
color = {v: WHITE for v in graph}
def dfs(v):
color[v] = GRAY # Mark as being processed
for neighbor in graph[v]:
if color[neighbor] == GRAY:
return True # Back edge to in-progress vertex → cycle!
if color[neighbor] == WHITE:
if dfs(neighbor):
return True
color[v] = BLACK # Mark as fully processed
return False
for v in graph:
if color[v] == WHITE:
if dfs(v):
return True
return False
# DAG — no cycle
dag = {'A':['B','C'], 'B':['D'], 'C':['D'], 'D':[]}
print(has_cycle_directed(dag)) # False
# Graph with cycle: A→B→C→A
cycle_g = {'A':['B'], 'B':['C'], 'C':['A']}
print(has_cycle_directed(cycle_g)) # True
DFS for Flood Fill / Island Problems
def count_islands(grid):
"""
Count connected islands (1s) in a 2D grid.
DFS from each unvisited land cell, marks entire island.
Time: O(m×n), Space: O(m×n) stack.
"""
m, n = len(grid), len(grid[0])
visited = [[False]*n for _ in range(m)]
count = 0
def dfs(r, c):
if r < 0 or r >= m or c < 0 or c >= n: return
if visited[r][c] or grid[r][c] == '0': return
visited[r][c] = True
dfs(r+1,c); dfs(r-1,c); dfs(r,c+1); dfs(r,c-1)
for r in range(m):
for c in range(n):
if not visited[r][c] and grid[r][c] == '1':
dfs(r, c)
count += 1
return count
grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
print(count_islands(grid)) # 3
Finding All Paths
def find_all_paths(graph, source, target):
"""Find all paths from source to target using DFS."""
all_paths = []
def dfs(node, path):
if node == target:
all_paths.append(list(path))
return
for neighbor in graph[node]:
if neighbor not in path: # Avoid cycles
path.append(neighbor)
dfs(neighbor, path)
path.pop() # Backtrack
dfs(source, [source])
return all_paths
graph = {0:[1,2], 1:[3], 2:[3], 3:[]}
print(find_all_paths(graph, 0, 3)) # [[0,1,3],[0,2,3]]
Finding Articulation Points (Cut Vertices)
An articulation point is a vertex whose removal increases the number of connected components.
def find_articulation_points(graph, n):
"""
Find all articulation points using Tarjan's DFS.
disc[v] = discovery time
low[v] = lowest discovery reachable via DFS subtree
"""
disc = [-1] * n
low = [-1] * n
parent = [-1] * n
ap = set()
timer = [0]
def dfs(u):
disc[u] = low[u] = timer[0]
timer[0] += 1
child_count = 0
for v in graph[u]:
if disc[v] == -1: # v not visited
child_count += 1
parent[v] = u
dfs(v)
low[u] = min(low[u], low[v])
# Root AP: has >= 2 children in DFS tree
if parent[u] == -1 and child_count > 1:
ap.add(u)
# Non-root AP: low[v] >= disc[u] means v can't reach ancestor of u
if parent[u] != -1 and low[v] >= disc[u]:
ap.add(u)
elif v != parent[u]:
low[u] = min(low[u], disc[v])
for i in range(n):
if disc[i] == -1:
dfs(i)
return ap
BFS vs DFS — When to Use Which
| Use BFS When | Use DFS When |
|---|
| Shortest path (unweighted) | Cycle detection |
| Level-by-level traversal | Topological sort |
| Find nearest node | Finding all paths |
| Bipartite check | Strongly connected components |
| Multi-source spread | Maze exploration (backtracking) |
| "Minimum hops" problems | "All possible configurations" |
Complexity
| Aspect | Recursive DFS | Iterative DFS |
|---|
| Time | O(V + E) | O(V + E) |
| Space | O(V) call stack | O(V) explicit stack |
| Stack overflow risk | Yes (deep recursion) | No |
| Order consistency | Natural left-to-right | Depends on push order |
Summary
DFS explores as deep as possible before backtracking. Use a stack (or recursion):
- Recursive: Cleaner code, risk of stack overflow for very deep graphs
- Iterative: No overflow risk, slightly more complex code
Key applications:
- Cycle detection: Gray vertices (in-progress) in directed graphs
- Connected components: Start DFS from each unvisited vertex
- Flood fill: DFS/BFS on 2D grids
- All paths: Backtracking with DFS
- Topological sort: Process vertices in reverse finish order (next lesson)
*Next Lesson: Dijkstra's Algorithm*