Skip to content
Computer Science

Graph Traversal

BFS and DFS — the two fundamental strategies for exploring connected data.

8 min read·May 10, 2026

On this page

Graphs are everywhere#

A graph is a collection of nodes (vertices) connected by edges. The abstraction is more powerful than it first appears:

  • Road networks: nodes are intersections, edges are roads.
  • Social networks: nodes are people, edges are friendships.
  • The web: nodes are pages, edges are hyperlinks.
  • Dependency graphs: nodes are packages, edges are requirements.
  • Molecular structures: nodes are atoms, edges are bonds.

Almost any problem involving relationships, connections, or reachability can be modeled as a graph problem. And the foundation of almost every graph algorithm is one of two traversal strategies: breadth-first search (BFS) or depth-first search (DFS).

The core question#

Given a graph and a starting node, visit every reachable node exactly once. The two algorithms differ in one fundamental choice: what to explore next?

BFS asks: explore the nearest unvisited nodes first. DFS asks: go as deep as possible before backtracking.

Switch between BFS and DFS. Watch how the Queue (BFS) and Stack (DFS) grow differently. BFS fans out in rings from the start — level by level. DFS plunges down one branch until it hits a dead end, then backtracks.

BFS: the queue#

BFS uses a FIFO queue (first in, first out). The algorithm:

  1. Enqueue the start node; mark it visited.
  2. Dequeue the front node; process it.
  3. Enqueue all unvisited neighbors; mark them visited.
  4. Repeat until queue is empty.
from collections import deque

def bfs(graph, start):
    visited = {start}
    queue = deque([start])
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return order

Key property: BFS visits nodes in order of their distance (number of edges) from the start. The first time BFS reaches a node, it has found the shortest path (by edge count) to that node.

This is BFS as a flood fill. Each cell is colored by its distance from the start, and you can watch the search expand outward one ring at a time — every cell in ring kk is reached before any cell in ring k+1k+1. That ordering is exactly why the first time the wave touches the goal it has found a shortest route, traced back in gold. It's the same algorithm behind unweighted shortest-path routing and maze solvers.

DFS: the stack#

DFS uses a LIFO stack (last in, first out). The recursive implementation is the most natural:

def dfs(graph, node, visited=None):
    if visited is None:
        visited = set()
    visited.add(node)
    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

The call stack is the stack. The iterative version makes this explicit — it uses an explicit stack data structure and produces the same traversal order.

When to use which#

BFS is optimal when you need:

  • Shortest path in an unweighted graph
  • Level-by-level processing (e.g., word ladders, 0-1 BFS)
  • Finding all nodes within a given distance

DFS is natural when you need:

  • Detecting cycles
  • Topological sorting (ordering dependencies)
  • Finding connected components
  • Maze solving (try one path completely before backtracking)
  • Tree traversals (preorder, inorder, postorder)

Shortest paths and BFS#

In an unweighted graph, BFS guarantees the shortest path from the start to every reachable node. The insight: BFS processes nodes in order of distance 0, then 1, then 2, then kk. The first time a node is reached, it's via the shortest path.

This makes BFS the foundation of many real-world algorithms: routing in networks, word-ladder puzzles ("change one letter at a time from COLD to WARM"), and the "six degrees of separation" computation in social graphs.

For weighted graphs, Dijkstra's algorithm generalizes BFS by using a priority queue instead of a regular queue — always expanding the node with the smallest cumulative distance.

Cycle detection and DFS#

DFS naturally detects cycles. As DFS recurses, it maintains a "currently in call stack" set. If it reaches a node that's already in this set, a cycle exists.

This is used in package managers to detect circular dependencies. If package A requires B, B requires C, and C requires A, the dependency graph has a cycle — and the package manager must detect this and report an error rather than entering an infinite installation loop.

Topological sort#

For directed acyclic graphs (DAGs), DFS computes a topological order: an ordering of nodes such that every edge goes from earlier to later in the ordering. This is the correct order to install dependencies — every package is installed only after all its requirements.

The DFS-based algorithm: run DFS, and when DFS finishes exploring a node (all its descendants are visited), append it to a result list. Reverse the result list. This is topological order.

Time and space complexity#

Both BFS and DFS visit every node and every edge exactly once: O(V+E)O(V + E) time where VV is vertices and EE is edges. This is optimal — you can't traverse a graph without looking at its edges.

Space differs: BFS stores the current frontier in its queue, which can be O(V)O(V) in the worst case (a star graph). DFS stores the current path in its call stack, which is O(depth)O(depth) — much smaller for wide, shallow graphs, but O(V)O(V) for a chain or deep tree.

The choice between BFS and DFS often comes down to memory constraints and the structure of the graph you're working with.

Key takeaways
  • Almost any problem about connections, reachability, or relationships can be modeled as a graph and attacked with one of two traversals.
  • BFS (a FIFO queue) explores in rings of increasing distance, so it finds shortest paths in unweighted graphs; DFS (a LIFO stack) plunges deep then backtracks.
  • DFS is the natural fit for cycle detection, topological sort, and connected components; BFS for shortest paths and level-order processing.
  • Both run in optimal O(V+E)O(V + E) time; they differ in memory — BFS holds an O(V)O(V) frontier, DFS an O(depth)O(\text{depth}) stack.
  • Add edge weights and BFS generalizes to Dijkstra's algorithm (a priority queue instead of a plain queue).
Check your understanding
1. Why does BFS guarantee the shortest path in an unweighted graph, while DFS does not?
2. When detecting circular dependencies in a package manager using DFS, what determines whether a cycle exists?
3. Why might DFS require significantly less memory than BFS when traversing a wide, shallow graph?
0 / 3 answered

Share this article

Share on X