DFS Topological Sort
Run DFS, record vertices as they finish, and reverse that list; a grey-to-grey edge during the search means a cycle.
Overview
The DFS approach to Topological Sort exploits a property of depth-first search on a DAG: a vertex finishes (its recursive call returns) only after every vertex reachable from it has finished. Therefore the list of vertices in finish order has every edge pointing *backwards*, and reversing it gives a topological order.
Cycle detection is folded into the same search with three colours: white (unvisited), grey (on the current recursion path), black (finished). Meeting a grey neighbour means the DFS found an edge back to an ancestor on the current path — a directed cycle — and no topological order exists.
The output is the order most DAG algorithms want as a by-product: reversed post-order is also the first half of Kosaraju's Algorithm and the backbone of DP on DAGs.
Intuition
A mental model before the formal terms.
You are assembling furniture from a manual that says "attach A only after B and C are done". Start with any piece: before writing it on the to-do list, dive into its dependencies and write those down first, recursively. Each piece gets written only when everything it needs is already written. Now you have a list where dependencies come *before* dependents — except that the DFS wrote things in finish order, which is dependents-last. Reverse it and read from the top.
The grey marker is a "currently working on it" sticky note. If, while resolving a piece's dependencies, you run into a piece that still has its sticky note on, you have gone in a circle.
Example: edges 0→1, 0→2, 1→3, 2→3. DFS from 0: enter 0, enter 1, enter 3, 3 finishes → post [3]; 1 finishes → [3, 1]; enter 2, 3 is black, 2 finishes → [3, 1, 2]; 0 finishes → [3, 1, 2, 0]. Reverse: [0, 2, 1, 3] — every edge points right.
How it works
- Colour all vertices white. Prepare an empty list
post. - For every white vertex
s, calldfs(s). dfs(u): colourugrey. For eachvinadj[u]: ifvis grey, a cycle exists — abort; ifvis white,dfs(v). After all neighbours, colourublack and appendutopost.- Return
reverse(post). Alternatively, prepend to a linked list or fill an array from the back to avoid the reverse. - Iterative version: push
(u, nextNeighbourIndex)frames on an explicit stack; on exhausting a frame's neighbours, colour black and append. Needed in Python/JavaScript for deep graphs.
Why it works
Consider any edge u → v in a DAG. When DFS scans this edge from u, v is either white (then dfs(v) runs to completion inside dfs(u), so v finishes first), or black (already finished, so again before u). v cannot be grey: grey means v is an ancestor of u on the current path, so a path v ⇝ u exists, and with u → v that is a cycle. Hence in a DAG finish(v) < finish(u) for every edge, and reversed finish order is topological.
Cycle detection is complete: if there is a cycle, the first vertex of the cycle that DFS discovers eventually scans the cycle edge leading to it while it is still grey (every other cycle vertex is reached inside its call).
Every vertex is coloured grey once and black once, every adjacency list is scanned once: O(V + E).
Recognition
How to tell a problem wants this.
- You need a topological order and are already writing a DFS for something else.
- The problem is naturally recursive: "to resolve X first resolve everything X depends on" (module loading, build graphs, symbol resolution).
- You also need to *find* a cycle, not just detect it — the grey vertices on the stack between the two ends of the back edge are the cycle.
Interactive visualization
Play, step, change the input. ← → and space work too.
1visited = {}; post = []2def dfs(u):3 visited.add(u)4 for v in neighbors(u):5 if v not in visited: dfs(v)6 post.append(u) # u finishes after all descendants7for u in nodes: if u not in visited: dfs(u)8return reversed(post)Pseudocode
1color[*] = WHITE; post = []2dfs(u):3 color[u] = GRAY4 for v in adj[u]:5 if color[v] == GRAY: cycle -> fail6 if color[v] == WHITE: dfs(v)7 color[u] = BLACK; post.append(u)8for s in 0..n-1: if color[s] == WHITE: dfs(s)9return reverse(post)Implementations
1def dfs_topo_sort(out: list[list[int]]) -> list[int]:2 """DFS topological sort: a vertex is emitted only after every vertex it3 can reach has been emitted, so pushing on *finish* and reversing gives a4 valid order. Iterative, because recursion depth is O(V). Returns [] when5 the graph has a cycle."""6 n = len(out)7 81 · Three colours: 0 = unvisited, 1 = on the current path, 2 = finished9 state = [0] * n10 order: list[int] = []11 12 for s in range(n):13 if state[s] != 0:14 continue15 162 · Each frame is [vertex, index of the next neighbour to explore]17 stack = [[s, 0]]18 state[s] = 119 while stack:20 frame = stack[-1]21 u, i = frame[0], frame[1]22 if i < len(out[u]):23 frame[1] += 124 v = out[u][i]253 · An edge into a state-1 vertex is a back edge: cyclic26 if state[v] == 1:27 return []28 if state[v] == 0:29 state[v] = 130 stack.append([v, 0])31 else:324 · All successors done, so u may now be emitted (post-order)33 state[u] = 234 order.append(u)35 stack.pop()36 375 · Post-order is reverse topological order38 return order[::-1]- Frames are two-element *lists* rather than tuples, because
frame[1] += 1must mutate the entry already on the stack. u, i = frame[0], frame[1]unpacks the frame for readability; the increment then writes back throughframe[1] += 1.- Vertices are appended on finish, and
order[::-1]reverses at the end to produce topological order. - The state-1 back-edge test returns
[]immediately on a cycle. - The iterative form avoids
RecursionError, which CPython raises near 1000 frames — reachable on a path graph of a few thousand vertices.
- A tuple frame is immutable, so
frame[1] += 1raisesTypeError; a list (or a small mutable class) is required. sys.setrecursionlimitcan raise the recursion cap but risks a genuine C-stack segfault — the explicit stack is the safe answer.order[::-1]returns a new list;order.reverse()reverses in place and returnsNone, which is a classic source of accidentalNonereturns.graphlib.TopologicalSorteris the standard-library alternative and uses the Kahn-style approach, raisingCycleError.
- Using a tuple for the frame and hitting
TypeErroron the cursor increment. - Writing
return order.reverse(), which returnsNonebecauselist.reversemutates in place. - Writing the recursive version and hitting
RecursionErroron a deep graph.
- Mutating the DFS frame cursor needs a different mechanism in each language: a C++ structured binding to a reference, a JavaScript/TypeScript object (references by nature), and a Python *list* — a tuple would raise.
- Recursion limits are why all four use an explicit stack, but the failure differs: CPython raises
RecursionErrornear 1000 frames, JS engines throwRangeErrornear 10000, and C++ overflows the stack with no diagnostic at all. - Signalling a cycle: TypeScript can return
nulland force the caller to handle it; C++, JavaScript and Python return an empty container that collides with the empty-graph case unless the caller checks separately. - Reversal: C++
std::reverseand JS/TSArray.prototype.reversemutate in place and are chainable; Pythonlist.reverse()returnsNone, so the slice[::-1]is the expression form.
Complexity
Recursion depth up to V; use the iterative variant for long chains.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- You want a topological order from code you are already writing as a DFS.
- You need to report the actual cycle (grey path segment) rather than just its existence.
- The reverse post-order itself is needed — e.g. as the first pass of Kosaraju's Algorithm or to evaluate a DAG DP.
- Lexicographically smallest order is required — use Kahn's Algorithm with a heap; DFS order depends on adjacency order in a non-obvious way.
- Level / round counting for parallel scheduling — Kahn's BFS layers give that directly.
- Extremely deep graphs in Python/JavaScript without converting to the iterative form.
Alternatives
Common mistakes
- Using only two states (visited / not) — then a cross edge to an already-finished vertex is misreported as a cycle. Grey vs black is the whole point.
- Appending on entry (pre-order) instead of on exit and reversing — pre-order is not a topological order (e.g.
0→2,0→1,1→2gives pre-order0,2,1). - Forgetting to reverse
post, returning dependents before dependencies. - Not restarting DFS from every white vertex, so disconnected parts of the DAG are missing from the order.
Interview patterns
- Course Schedule: return
falseon a grey neighbour; return the reversed post-order for Schedule II. - Longest path in a DAG: relax edges in topological order, or memoise
best(u) = 1 + max(best(v))— the memoised DFS *is* this algorithm. - Find Eventual Safe States: vertices that finish black without touching a grey vertex are safe.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Stack versus queueBeginner
- Recursion versus iterationIntermediate
- When space complexity mattersIntermediate
- Course ScheduleIntermediate
- Number of IslandsIntermediate
- Word SearchAdvanced