🚀 Phases 1–5 are live — Days 1–17 cover the foundations and the algorithmic patterns. See the roadmap →
Day 23 - Topological SortDFS Topological Sort

DFS Topological Sort

The second topological-sort algorithm. Once you have a DFS template in your fingers, this version is a four-line addition.

The intuition: recurse depth-first. When a node’s recursion finishes (all of its descendants are emitted), emit the node itself. Then reverse the output at the end.

Why does that work? When DFS at u finishes, every node reachable from u has been emitted before u in the output list. So when we reverse, u appears before every node reachable from u — which is exactly the topological-order requirement.

The template

state[v] in { WHITE, GRAY, BLACK }   for every v
output = []

def dfs(u):
    state[u] = GRAY
    for each edge u -> v:
        if state[v] == GRAY:  cycle detected
        if state[v] == WHITE: dfs(v)
    state[u] = BLACK
    output.append(u)

for each node u with state[u] == WHITE:
    dfs(u)

return reversed(output)

The three colors:

  • WHITE — not yet visited.
  • GRAY — currently being processed (on the DFS recursion stack).
  • BLACK — fully processed (recursion finished).

Seeing a GRAY descendant during the loop means we’ve looped back to an ancestor on the current DFS path — that’s a back edge, and back edges exist if and only if there’s a cycle.

The full code

Time: O(V + E). Space: O(V + E) for the adjacency list and recursion stack.

Why the reversal?

When we out.append(u) after the recursion at u finishes, every node reachable from u is already in out. So in the forward order, descendants come before their ancestors — opposite of topological order. Reverse at the end and you get the right order.

A common alternative: prepend to the output list (no reversal needed). Less efficient if out is a Python list (O(n) prepend), but conceptually identical:

out.insert(0, u)              # equivalent to appending then reversing

Or use a stack in the formal sense — push during post-order, pop at the end — same idea.

Stepping through an example

Same graph as the Kahn’s page: edges 5→0, 5→2, 4→0, 4→1, 2→3, 3→1.

Starting DFS from node 0 (the smallest):

  1. dfs(0) — state[0] = GRAY. No out-edges. state[0] = BLACK. out = [0].

Starting DFS from node 1:

  1. dfs(1) — same. out = [0, 1].

Node 2:

  1. dfs(2) — state[2] = GRAY. Has edge 2 → 3.
    • dfs(3) — state[3] = GRAY. Has edge 3 → 1. state[1] is BLACK, skip. state[3] = BLACK. out = [0, 1, 3].
    • Return to dfs(2). state[2] = BLACK. out = [0, 1, 3, 2].

Node 3 is BLACK — skip. Node 4:

  1. dfs(4) — state[4] = GRAY. Edges 4 → 0 (skip BLACK), 4 → 1 (skip BLACK). state[4] = BLACK. out = [0, 1, 3, 2, 4].

Node 5:

  1. dfs(5) — state[5] = GRAY. Edges 5 → 0 (skip), 5 → 2 (skip). state[5] = BLACK. out = [0, 1, 3, 2, 4, 5].

Reverse: [5, 4, 2, 3, 1, 0]. Valid topo order. Different from Kahn’s output, equally correct.

DFS vs Kahn’s — head to head

AspectKahn’s (BFS)DFS post-order
IterativeYes — uses a queueNo — recursive
Stack depthNoneO(V) worst case
Cycle detectionlen(out) != VGRAY descendant during recursion
Order producedEarliest-eligible firstReverse post-order — “leaf-deepest” first
Easy to make lex-smallestYes — heap instead of queueHarder
Easy to track parallel levelsYes — BFS roundsHarder
Edge in/out direction sensitiveYesYes

The recursive DFS version has a hidden trap: deep DAGs can blow the stack. A DAG that’s effectively a long chain 1 → 2 → 3 → ... → n will recurse n levels deep. For n in the millions, that exceeds most language defaults. Kahn’s has no such limit.

If you specifically want DFS but need to handle deep chains, convert to an explicit stack:

def topo_sort_dfs_iterative(n, edges):
    adj = [[] for _ in range(n)]
    for u, v in edges:
        adj[u].append(v)
    state = [0] * n              # 0 white, 1 gray, 2 black
    out = []
    for start in range(n):
        if state[start] != 0: continue
        stack = [(start, 0)]      # (node, next-neighbor-index)
        while stack:
            u, i = stack[-1]
            if i == 0:
                if state[u] == 1: return []   # cycle
                state[u] = 1
            if i < len(adj[u]):
                v = adj[u][i]
                stack[-1] = (u, i + 1)
                if state[v] == 1: return []   # back edge -> cycle
                if state[v] == 0:
                    stack.append((v, 0))
            else:
                state[u] = 2
                out.append(u)
                stack.pop()
    return out[::-1]

Same algorithm, no recursion stack. Uglier code — Kahn’s is the easier path for “no recursion.”

Quick check

Why do we use THREE colors (WHITE/GRAY/BLACK) in DFS topo sort instead of just visited/unvisited?
Why do we REVERSE the output at the end of DFS topo sort?
A deep DAG of n = 10^6 nodes shaped like a chain. Should you use the recursive DFS or Kahn's?

Next: cycle detection — the three-color algorithm and Kahn’s complementary technique, plus how to actually report the cycle.