Views

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)

Pathfinding (30%)
Dependency Resolution (25%)
Cycle Detection (20%)
Backtracking (15%)
Other (10%)

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

1
Start at source node
  • Mark current node as visited
  • Process the node (print, record, etc.)
2
Explore each neighbor
  • For each adjacent unvisited node
  • Recursively (or iteratively) explore it
  • Go as deep as possible before backtracking
3
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

1
Visit root
  • Process current node first
2
Traverse left subtree
  • Recursively apply pre-order to left child
3
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

1
Track three states
  • Unvisited: node not yet visited
  • Visiting: node in current recursion stack
  • Visited: node fully processed
2
DFS with recursion stack
  • When encountering a "visiting" node → cycle found
  • Mark node as visiting before exploring neighbors
3
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

1
Perform DFS on each unvisited node
  • Traverse graph using DFS
2
Push node to stack after processing
  • Post-order: add to stack when leaving node
  • Ensures dependencies come first
3
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

1
Make a choice
  • Select a candidate from available options
  • Add to current solution path
2
Recursive exploration
  • Continue building solution recursively
  • Check if current state is promising
3
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

1
Scan grid for unvisited land
  • Find cell with value '1' not yet visited
  • Increment island count
2
DFS to mark entire island
  • Explore all four directions
  • Mark visited cells (set to '0' or use visited set)
3
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

Track visited nodes to avoid infinite loops
For directed cycles, maintain recursion stack separately
For backtracking, remember to undo choices
Use iterative stack for deep recursion to avoid stack overflow
Consider reverse adjacency order for certain traversals
For topological sort, add nodes to result after processing children
In grid problems, check boundaries before recursive calls

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.

Share and Join the Discussion

You need to be logged in to participate in this discussion.

×
×