Graph Algorithms

How Depth-First Search Works

Depth-first search walks as far as it can down one path before backtracking, using a stack — or the call stack. The same loop as BFS with a different container, and a completely different traversal.

ABCDEFGH
On the stack
  1. A
Visit order
  1. A
  • Not reached
  • Waiting
  • Current
  • Finished
Speed

Step 1 of 20

Start at A. Depth-first follows one branch as far as it goes.

Counters

Visited
1/8
On the stack
1
Nodes
8
Edges
10

Settings

Graph

Depth-first

  1. 1stack = [start]
  2. 2while stack:
  3. 3 node = stack.top
  4. 4 if node has an unseen neighbour:
  5. 5 mark it seen
  6. 6 stack.push(it)
  7. 7 else:
  8. 8 stack.pop() // done here

What each one answers

AlgorithmWhat it answersTimeNeeds
Breadth-firstFewest hops from the startO(V + E)A queue
Depth-firstReachability, one branch at a timeO(V + E)A stack
Cycle detectionDoes a cycle exist, and whereO(V + E)Node colours
Topological sortA valid order to do the work inO(V + E)No cycles
DijkstraCheapest path by edge weightO(E log V)Non-negative weights

V is the number of nodes and E the number of edges. Switch to the graph with a cycle and the topological sort gets stuck: once a cycle exists there is no valid order.

Complexity

BestAverageWorstExtra memory
O(V + E)O(V + E)O(V + E)O(V)

Same bound as BFS. Recursion depth can reach V, so a long path in a large graph will overflow a call stack that an explicit stack would survive.

How depth-first search works

Depth-first search picks a neighbour and commits to it, going deeper until it reaches a vertex with nothing new to visit, then backtracks to the most recent vertex that still has unexplored neighbours. Swap BFS's queue for a stack and you have it — which is why the two are usually written as the same loop. The order it finishes vertices in, rather than the order it discovers them, is what makes it the basis for topological sorting and cycle detection.

  1. Mark the start visited.
  2. Pick an unvisited neighbour and descend into it, repeating this rule.
  3. When a vertex has no unvisited neighbours, it is finished — back up to its predecessor.
  4. Continue until every vertex reachable from the start is finished.

Implementation

The same algorithm the visualiser is running, written to be read.

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


def dfs_iterative(graph, start):
    """BFS's loop with a stack instead of a queue - that is the only change."""
    visited, stack = set(), [start]
    while stack:
        node = stack.pop()
        if node in visited:
            continue
        visited.add(node)
        for neighbour in reversed(graph[node]):
            if neighbour not in visited:
                stack.append(neighbour)
    return visited

When to use it

  • Use it to detect cycles, to find strongly connected components, and to topologically sort — all of which depend on the order vertices finish in.
  • Use it when the graph is deep and you just need to reach something rather than reach it quickly, or when memory is tight: DFS holds one path, BFS holds one level.
  • Use the iterative form for large graphs; recursion depth is the practical limit on the recursive one.
  • Avoid it for shortest paths. The first path DFS finds is often the longest one.

Check yourself

If the animation made sense, these should too.

Why is DFS the basis for cycle detection and topological sort, when BFS is not?

Because those problems depend on the order vertices *finish* in, not the order they are discovered. DFS's recursion gives a finish time per vertex for free; BFS has no equivalent notion.

When is the iterative version not just a style preference?

When the graph is deep. Recursion depth can reach the number of vertices, and a long path in a large graph overflows the call stack - a crash an explicit stack on the heap simply does not have.

More graph algorithms