DSA Graph Traversal

Graph traversal algorithms visit every reachable vertex in a graph, and the two fundamental strategies are breadth-first search (BFS) and depth-first search (DFS).

Why Traverse a Graph?

Traversal means systematically visiting every vertex reachable from a starting point, exactly once, so you can search for a target, list connected components, or explore the graph's structure. Because graphs can contain cycles, every traversal algorithm must track which vertices have already been visited, or it can loop forever.

Breadth-First Search (BFS)

BFS explores a graph level by level: it visits the start vertex, then all of its direct neighbors, then all of their unvisited neighbors, and so on outward. It uses a queue (first-in, first-out) to decide which vertex to expand next. Because it expands outward one layer at a time, BFS is the standard choice for finding the shortest path measured in number of edges within an unweighted graph.

Example 1: BFS with a Queue

from collections import deque

def bfs(graph, start):
    visited = {start}
    queue = deque([start])
    order = []

    while queue:
        vertex = queue.popleft()
        order.append(vertex)
        for neighbor in graph[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

    return order

graph = {
    "A": ["B", "C"],
    "B": ["A", "D"],
    "C": ["A", "D"],
    "D": ["B", "C", "E"],
    "E": ["D"],
}

print(bfs(graph, "A"))  # ['A', 'B', 'C', 'D', 'E']

Depth-First Search (DFS)

DFS explores as far as possible down one branch before backtracking, using a stack (last-in, first-out) instead of a queue. It can be written recursively, where the function call stack plays the role of the stack, or iteratively with an explicit stack. DFS fits naturally with tasks like detecting cycles, finding connected components, and topological sorting.

Example 2: DFS, Recursive

def dfs_recursive(graph, vertex, visited=None, order=None):
    if visited is None:
        visited = set()
        order = []

    visited.add(vertex)
    order.append(vertex)

    for neighbor in graph[vertex]:
        if neighbor not in visited:
            dfs_recursive(graph, neighbor, visited, order)

    return order

graph = {
    "A": ["B", "C"],
    "B": ["A", "D"],
    "C": ["A", "D"],
    "D": ["B", "C", "E"],
    "E": ["D"],
}

print(dfs_recursive(graph, "A"))  # ['A', 'B', 'D', 'C', 'E']

Example 3: DFS, Iterative

def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    order = []

    while stack:
        vertex = stack.pop()
        if vertex in visited:
            continue
        visited.add(vertex)
        order.append(vertex)
        for neighbor in reversed(graph[vertex]):
            if neighbor not in visited:
                stack.append(neighbor)

    return order

print(dfs_iterative(graph, "A"))  # ['A', 'B', 'D', 'C', 'E']
  • Shortest path by edge count in an unweighted graph: use BFS.
  • Exploring deep structures with limited memory for the frontier, such as maze solving: use DFS.
  • Detecting cycles or finding connected components: either works; DFS is often simpler to write recursively.
  • Topological sorting of a directed acyclic graph: use DFS.
  • Finding all vertices within a fixed number of hops of a start vertex: use BFS.
AspectBFSDFS
Data structureQueue (FIFO)Stack (LIFO) or recursion
Traversal orderLevel by level, outward from startDeep along one branch before backtracking
Shortest path (unweighted)Yes, guarantees fewest edgesNo guarantee
Typical memory useCan be higher (stores a full frontier)Can be lower (stores one path at a time)
Note: Mark a vertex as visited the moment you add it to the queue or stack, not when you pop it. Marking only on pop lets the same vertex be enqueued multiple times before it is ever processed, which wastes time and can distort BFS's level-by-level guarantee.

Exercise: DSA Graph Traversal

Which data structure does BFS use to track which nodes to visit next?