Skip to content

Graph Traversal: BFS and DFS

BFS (Breadth-First Search) and DFS (Depth-First Search) are the two fundamental graph traversal algorithms. Both visit every vertex in O(|V| + |E|) time but differ in order, data structure, and applications. BFS finds shortest paths in unweighted graphs; DFS is used for cycle detection, topological sort, and connected components.

Key Facts

  • DFS: Uses stack (or recursion). Goes deep before backtracking. Space O(max depth)
  • BFS: Uses queue. Explores level by level. Space O(max breadth)
  • Both: O(|V| + |E|) time with adjacency list, O(|V|^2) with adjacency matrix
  • BFS guarantees shortest path in unweighted graphs
  • DFS preferred for cycle detection, topological sort, finding all paths

Choosing DFS vs BFS

Use Case Choose
Shortest path (unweighted) BFS
Checking connectivity Either
Cycle detection DFS
Topological sort DFS (or BFS with Kahn's)
Finding all paths DFS (with backtracking)
Level-order / layer problems BFS
Very deep graph DFS (if depth manageable)
Very wide graph DFS (BFS queue gets huge)

Patterns

Iterative DFS

def dfs(graph, start):
    visited = set()
    stack = [start]
    while stack:
        vertex = stack.pop()
        if vertex not in visited:
            visited.add(vertex)
            print(vertex)  # process
            for neighbor in graph.adj_list[vertex]:
                if neighbor not in visited:
                    stack.append(neighbor)

Recursive DFS

def dfs(graph, start, visited=None):
    if visited is None:
        visited = set()
    visited.add(start)
    print(start)  # process
    for neighbor in graph.adj_list[start]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

BFS

from collections import deque

def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)  # mark when enqueuing, not dequeuing
    while queue:
        vertex = queue.popleft()
        print(vertex)
        for neighbor in graph.adj_list[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

DFS Path Existence

def has_path(graph, src, dest):
    visited = set()
    stack = [src]
    while stack:
        vertex = stack.pop()
        if vertex == dest:
            return True
        if vertex not in visited:
            visited.add(vertex)
            for neighbor in graph.adj_list[vertex]:
                if neighbor not in visited:
                    stack.append(neighbor)
    return False

BFS Shortest Path (Unweighted)

from collections import deque

def shortest_path(graph, src, dest):
    visited = {src}
    queue = deque([(src, [src])])
    while queue:
        vertex, path = queue.popleft()
        if vertex == dest:
            return path
        for neighbor in graph.adj_list[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))
    return None

Implicit Graph: Keys and Rooms

def can_visit_all_rooms(rooms):
    visited = set()
    stack = [0]
    while stack:
        room = stack.pop()
        if room not in visited:
            visited.add(room)
            for key in rooms[room]:
                if key not in visited:
                    stack.append(key)
    return len(visited) == len(rooms)

Multi-Source BFS: Rotten Oranges

All rotten oranges start simultaneously. Each BFS level = 1 minute.

from collections import deque

def oranges_rotting(grid):
    rows, cols = len(grid), len(grid[0])
    queue = deque()
    fresh = 0
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                queue.append((r, c, 0))
            elif grid[r][c] == 1:
                fresh += 1
    if fresh == 0:
        return 0
    max_time = 0
    for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
        pass  # directions used in loop below
    while queue:
        r, c, time = queue.popleft()
        for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                grid[nr][nc] = 2
                fresh -= 1
                max_time = max(max_time, time + 1)
                queue.append((nr, nc, time + 1))
    return max_time if fresh == 0 else -1

Gotchas

  • BFS: mark visited when enqueuing, not when dequeuing - prevents duplicate processing
  • Iterative DFS processes vertices in different order than recursive DFS (stack reverses neighbor order)
  • Recursive DFS risks stack overflow on deep graphs (Python default limit: 1000)
  • Grid problems are implicit graphs - neighbors defined by directions, not explicit adjacency list
  • Multi-source BFS: enqueue ALL sources at time 0, not one at a time

See Also

  • [[graph-representation]] - adjacency list and matrix implementations
  • [[shortest-path-algorithms]] - weighted graph shortest paths (Dijkstra, Bellman-Ford)
  • [[topological-sort]] - DFS and BFS-based topological ordering
  • [[trees-and-binary-trees]] - tree traversals as special case of graph traversal