Report Content
×Depth-First Search (DFS): Complete Guide
Master DFS for pathfinding, cycle detection, and backtracking. Covers recursive/iterative implementations, topological sorting, and advanced optimizations with real-world applications.
DFS Usage in Tech (2023)
1. DFS Core Concepts
Depth-First Search (DFS)
Definition
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It uses a stack (either recursive call stack or explicit stack) and is ideal for pathfinding, cycle detection, and backtracking problems.
How It Works
Start at source node
- Mark current node as visited
- Process the node (print, record, etc.)
Explore each neighbor
- For each adjacent unvisited node
- Recursively (or iteratively) explore it
- Go as deep as possible before backtracking
Backtrack
- When no more unvisited neighbors
- Return to previous node
- Continue with remaining neighbors
Recursive DFS Implementation
def dfs_recursive(graph, node, visited=None):
if visited is None:
visited = set()
visited.add(node)
print(node, end=' ') # Process node
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
return visited
# Example graph
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
print("DFS starting from 'A':")
dfs_recursive(graph, 'A') # Output: A B D E F C
Iterative DFS Implementation
def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
print(node, end=' ') # Process node
# Add neighbors to stack (reverse for original order)
for neighbor in reversed(graph[node]):
if neighbor not in visited:
stack.append(neighbor)
return visited
# Example
print("\nIterative DFS from 'A':")
dfs_iterative(graph, 'A') # Output: A C F B E D
Complexity: O(V + E) time for adjacency list, O(V) space
Use When: Pathfinding, cycle detection, topological sort
2. Binary Tree DFS Traversals
Pre-Order Traversal (Root ā Left ā Right)
Definition
Pre-Order Traversal visits the root node first, then recursively traverses the left subtree, then the right subtree. It's used for copying trees and prefix expression evaluation.
How It Works
Visit root
- Process current node first
Traverse left subtree
- Recursively apply pre-order to left child
Traverse right subtree
- Recursively apply pre-order to right child
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def preorder_recursive(root):
result = []
def dfs(node):
if not node:
return
result.append(node.val) # Visit root
dfs(node.left) # Traverse left
dfs(node.right) # Traverse right
dfs(root)
return result
def preorder_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# Push right first so left is processed first
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
# Example: Tree: 1 -> 2,3; 2 -> 4,5
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
print(preorder_recursive(root)) # [1, 2, 4, 5, 3]
In-Order Traversal (Left ā Root ā Right)
Definition
In-Order Traversal visits the left subtree first, then the root, then the right subtree. For BSTs, it produces nodes in sorted order.
def inorder_recursive(root):
result = []
def dfs(node):
if not node:
return
dfs(node.left)
result.append(node.val)
dfs(node.right)
dfs(root)
return result
def inorder_iterative(root):
result = []
stack = []
current = root
while stack or current:
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
print(inorder_recursive(root)) # [4, 2, 5, 1, 3]
Post-Order Traversal (Left ā Right ā Root)
Definition
Post-Order Traversal visits the left subtree, then the right subtree, then the root. It's used for deleting trees and computing expressions.
def postorder_recursive(root):
result = []
def dfs(node):
if not node:
return
dfs(node.left)
dfs(node.right)
result.append(node.val)
dfs(root)
return result
def postorder_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return result[::-1] # Reverse for post-order
print(postorder_recursive(root)) # [4, 5, 2, 3, 1]
3. Cycle Detection in Graphs
Detect Cycle in Directed Graph
Definition
Cycle Detection identifies if a graph contains cycles. For directed graphs, we track nodes in current recursion stack; for undirected graphs, we track parent nodes to avoid false cycles.
How It Works
Track three states
- Unvisited: node not yet visited
- Visiting: node in current recursion stack
- Visited: node fully processed
DFS with recursion stack
- When encountering a "visiting" node ā cycle found
- Mark node as visiting before exploring neighbors
Mark as visited after exploration
- Remove from recursion stack when done
- Continue with other nodes
def has_cycle_directed(graph):
visited = set() # Fully processed
rec_stack = set() # Currently in recursion stack
def dfs(node):
if node in rec_stack:
return True # Cycle detected
if node in visited:
return False
visited.add(node)
rec_stack.add(node)
for neighbor in graph[node]:
if dfs(neighbor):
return True
rec_stack.remove(node)
return False
for node in graph:
if dfs(node):
return True
return False
def has_cycle_undirected(graph):
visited = set()
def dfs(node, parent):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
if dfs(neighbor, node):
return True
elif neighbor != parent:
return True
return False
for node in graph:
if node not in visited:
if dfs(node, -1):
return True
return False
# Example with cycle
graph_with_cycle = {
0: [1],
1: [2],
2: [0] # Creates cycle 0-1-2-0
}
print(has_cycle_directed(graph_with_cycle)) # True
Complexity: O(V + E) time, O(V) space
Use When: Deadlock detection, dependency validation
4. Topological Sort (DFS-based)
Topological Ordering of DAG
Definition
Topological Sort orders vertices in a Directed Acyclic Graph (DAG) such that for every directed edge uāv, u appears before v. It's used for task scheduling and dependency resolution.
How It Works
Perform DFS on each unvisited node
- Traverse graph using DFS
Push node to stack after processing
- Post-order: add to stack when leaving node
- Ensures dependencies come first
Pop stack for topological order
- Stack gives reverse topological order
- Pop to get correct ordering
def topological_sort(graph):
visited = set()
stack = []
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
stack.append(node) # Post-order
for node in graph:
if node not in visited:
dfs(node)
return stack[::-1] # Reverse for topological order
# Example: Course prerequisites
courses = {
'C1': [], # No prerequisites
'C2': ['C1'], # Needs C1 first
'C3': ['C1'], # Needs C1 first
'C4': ['C2', 'C3'] # Needs C2 and C3
}
order = topological_sort(courses)
print(order) # Possible order: ['C1', 'C2', 'C3', 'C4']
Complexity: O(V + E) time, O(V) space
Use When: Task scheduling, dependency resolution
5. Backtracking with DFS
General Backtracking Template
Definition
Backtracking is an algorithmic technique for solving problems recursively by building candidates incrementally and abandoning (backtracking) partial candidates when they can't lead to a valid solution.
How It Works
Make a choice
- Select a candidate from available options
- Add to current solution path
Recursive exploration
- Continue building solution recursively
- Check if current state is promising
Undo choice (backtrack)
- Remove choice from current path
- Try next candidate
All Subsets (Power Set)
def subsets(nums):
result = []
def backtrack(start, current):
result.append(current[:]) # Add copy of current path
for i in range(start, len(nums)):
current.append(nums[i])
backtrack(i + 1, current)
current.pop() # Backtrack
backtrack(0, [])
return result
print(subsets([1, 2, 3]))
# [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]
All Permutations
def permutations(nums):
result = []
used = [False] * len(nums)
def backtrack(current):
if len(current) == len(nums):
result.append(current[:])
return
for i in range(len(nums)):
if not used[i]:
used[i] = True
current.append(nums[i])
backtrack(current)
current.pop() # Backtrack
used[i] = False
backtrack([])
return result
print(permutations([1, 2, 3]))
# [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
Complexity: O(n! Ć n) for permutations, O(2^n) for subsets
Use When: Combinatorial problems, constraint satisfaction
6. Connected Components (Number of Islands)
Finding Connected Components in Grid
Definition
Number of Islands problem counts connected components in a 2D grid. DFS explores all four directions (up, down, left, right) to mark connected land cells.
How It Works
Scan grid for unvisited land
- Find cell with value '1' not yet visited
- Increment island count
DFS to mark entire island
- Explore all four directions
- Mark visited cells (set to '0' or use visited set)
Repeat until grid is scanned
- Continue scanning for more land
- Each DFS marks one complete island
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
visited = set()
def dfs(r, c):
# Check boundaries and conditions
if (r < 0 or r >= rows or c < 0 or c >= cols or
grid[r][c] == '0' or (r, c) in visited):
return
visited.add((r, c))
# Explore all 4 directions
directions = [(1,0), (-1,0), (0,1), (0,-1)]
for dr, dc in directions:
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
# Example grid
grid = [
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
print(num_islands(grid)) # 3
Complexity: O(rows Ć cols) time and space
Use When: Grid connectivity, image segmentation
DFS Problem-Solving Cheat Sheet
| Problem Type | DFS Approach | Complexity |
|---|---|---|
| Connected Components/Islands | DFS on each unvisited node | O(V + E) |
| Cycle Detection (Directed) | Track recursion stack | O(V + E) |
| Cycle Detection (Undirected) | Track parent node | O(V + E) |
| Topological Sort | Post-order DFS, reverse | O(V + E) |
| Path Existence | DFS from start to target | O(V + E) |
| Tree Traversals | Pre/In/Post-order | O(N) |
| Backtracking | Try, recurse, undo | O(b^d) |
DFS Implementation Checklist
7. Real-World Applications
Web Crawler
DFS-based crawling: Explore links recursively
class WebCrawler:
def __init__(self):
self.visited = set()
def crawl(self, url, max_depth=3):
if url in self.visited or max_depth == 0:
return []
self.visited.add(url)
pages = [url]
for link in self.get_links(url):
pages.extend(self.crawl(link, max_depth - 1))
return pages
def get_links(self, url):
# Simulate getting links from page
return []
Maze Solving
Path finding: DFS to find exit
def solve_maze(maze, start, end):
rows, cols = len(maze), len(maze[0])
path = []
def dfs(r, c):
if (r, c) == end:
path.append((r, c))
return True
if (r < 0 or r >= rows or c < 0 or c >= cols or
maze[r][c] == 1): # 1 = wall
return False
maze[r][c] = 1 # Mark as visited
path.append((r, c))
# Try all 4 directions
for dr, dc in [(1,0), (-1,0), (0,1), (0,-1)]:
if dfs(r + dr, c + dc):
return True
path.pop() # Backtrack
return False
dfs(start[0], start[1])
return path
Expert Tip: DFS shines in problems requiring exhaustive search or path exploration. For shortest path in unweighted graphs, prefer BFS. Combine DFS with memoization for dynamic programming problems (DFS+DP). For real-world applications, always set a maximum recursion depth to prevent stack overflow, or use iterative implementation with explicit stack.
You need to be logged in to participate in this discussion.