Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Graphs

Depth-first traversal

Lesson · 10 min read · Graphs · Traversing a graph

Follow one branch to its end before trying the next: the recursive search behind cycle detection, components and topological order.

One branch at a time

An array is traversed with a loop because its items sit in a line. A graph has no line: from any node there may be several ways on, and some lead back to nodes already seen. A traversal needs a rule for which way to go next and a record of where it has been.

Depth-first search takes the simplest rule. From the current node, step to a neighbour that has not been visited and carry on from there. Only when a node has no unvisited neighbour left does the search back up to the node it came from and try that node's next neighbour.

0123456
From 0 the search goes down to 1 and 3, backs up to 1, then goes to 4. The whole left branch is finished before 2 is touched.

On a tree this is exactly preorder traversal. On a graph the one addition is the visited set, because a cycle would otherwise send the search round for ever.

The algorithm

The rule is naturally recursive: searching from u means marking u and then searching from each of its unvisited neighbours in turn. The call stack remembers the way back.

Depth-first traversal

dfs(u)

  1. Add u to visited (and do whatever the problem needs with it).
  2. For each neighbour v of u: if v is not in visited, call dfs(v).

traverse(graph)

  1. Create an empty visited set.
  2. For each node s of the graph: if s is not in visited, call dfs(s).

The outer loop matters. One call to dfs reaches only the nodes connected to where it started; if the graph is in several pieces, the rest stay unvisited.

0123456outer loop→0123456
Left: dfs(0) reaches 0 to 4 and stops. Right: the outer loop finds 5 unvisited and calls dfs(5).

Step by step

Here is the traversal on that graph, with neighbours tried in increasing order. Watch the call stack: it grows as the search goes deeper and shrinks as it backtracks, and its contents are always the path from the start to the current node.

Depth-first traversal from node 0. The call stack is shown bottom to top.

  1. Step 1 / 15
    0123456
    call stack (bottom → top)
    00
    visit order
    00

    Start a new search at 0: mark it visited and push it on the call stack.

  2. Step 2 / 15
    0123456
    call stack (bottom → top)
    0011
    visit order
    0011

    From 0, go to its first unvisited neighbour, 1. Mark it and push it.

  3. Step 3 / 15
    0123456
    call stack (bottom → top)
    001132
    visit order
    001132

    From 1, go to its first unvisited neighbour, 3. Mark it and push it.

  4. Step 4 / 15
    0123456
    call stack (bottom → top)
    00113223
    visit order
    00113223

    From 3, go to its first unvisited neighbour, 2. Mark it and push it.

  5. Step 5 / 15
    0123456
    call stack (bottom → top)
    001132
    visit order
    00113223

    Every neighbour of 2 is visited: 2 is finished and popped, so the search backtracks to 3.

  6. Step 6 / 15
    0123456
    call stack (bottom → top)
    00113243
    visit order
    0011322344

    From 3, go to its first unvisited neighbour, 4. Mark it and push it.

  7. Step 7 / 15
    0123456
    call stack (bottom → top)
    001132
    visit order
    0011322344

    Every neighbour of 4 is visited: 4 is finished and popped, so the search backtracks to 3.

  8. Step 8 / 15
    0123456
    call stack (bottom → top)
    0011
    visit order
    0011322344

    Every neighbour of 3 is visited: 3 is finished and popped, so the search backtracks to 1.

  9. Step 9 / 15
    0123456
    call stack (bottom → top)
    00
    visit order
    0011322344

    Every neighbour of 1 is visited: 1 is finished and popped, so the search backtracks to 0.

  10. Step 10 / 15
    0123456
    call stack (bottom → top)
    empty0
    visit order
    0011322344

    Every neighbour of 0 is visited: 0 is finished and popped.

  11. Step 11 / 15
    0123456
    call stack (bottom → top)
    empty0
    visit order
    0011322344

    The search from the previous start is exhausted but 5 is unvisited: this graph is not connected, so the outer loop starts again at 5.

  12. Step 12 / 15
    0123456
    call stack (bottom → top)
    50
    visit order
    001132234455

    Start a new search at 5: mark it visited and push it on the call stack.

  13. Step 13 / 15
    0123456
    call stack (bottom → top)
    5061
    visit order
    00113223445566

    From 5, go to its first unvisited neighbour, 6. Mark it and push it.

  14. Step 14 / 15
    0123456
    call stack (bottom → top)
    50
    visit order
    00113223445566

    Every neighbour of 6 is visited: 6 is finished and popped, so the search backtracks to 5.

  15. Step 15 / 15
    0123456
    call stack (bottom → top)
    empty0
    visit order
    00113223445566

    Every neighbour of 5 is visited: 5 is finished and popped. Every node is visited. Order: 0, 1, 3, 2, 4, 5, 6.

current nodeon the call stackfinishededge the search followed

Implementation

The graph is an adjacency list. The recursive function needs the list and the visited set; in the languages below they are captured from the enclosing function or passed by reference, so every call shares one copy. The order of visits is recorded so the result can be checked.

def dfs_order(n, edges):
    adj = [[] for _ in range(n)]
    for u, v in edges:
        adj[u].append(v)
        adj[v].append(u)            # undirected: store both directions
    for nb in adj:
        nb.sort()                   # smallest neighbour first
    seen = [False] * n
    order = []

    def dfs(u):
        seen[u] = True              # mark on entry
        order.append(u)
        for v in adj[u]:
            if not seen[v]:
                dfs(v)

    for s in range(n):              # one search per unvisited part
        if not seen[s]:
            dfs(s)
    return order

The same code is the reference solution to Depth-first order of a graph, where it is compiled and run against the tests in all six languages.

Without recursion, an explicit stack does the same job. To visit nodes in the same order as the recursive version, the stack must remember how far through each node's neighbours the search has got:

def dfs_iterative(adj, s, visited):
    visited.add(s)
    stack = [(s, 0)]                     # (node, index of the next neighbour to try)
    while stack:
        u, i = stack[-1]
        if i == len(adj[u]):
            stack.pop()                  # every neighbour tried: backtrack
            continue
        stack[-1] = (u, i + 1)
        v = adj[u][i]
        if v not in visited:
            visited.add(v)
            stack.append((v, 0))

The common shortcut, popping a node and pushing all its neighbours at once, also visits every node, but in a different order. That is fine for counting components and wrong for anything that depends on the recursive order, such as finishing times.

Complexity

Each node is entered once, because it is marked before its neighbours are explored. Each edge is looked at twice in an undirected graph (once from each end) and once in a directed one. Building the adjacency list is O(n + m) as well.

TimeO(n + m)
SpaceO(n)

The visited set holds up to n nodes. The call stack is as deep as the longest path the search follows: 1 for a star, n for a path. A path of 10⁵ nodes overflows the default stack in Python (limit 1,000 frames), and can overflow it in C++ and Java; use the iterative form for inputs that large.