Topological Sort
Imagine a university registrar sequencing courses. No student should take a class before its prerequisites. Or imagine a build system compiling files. Every module must be built after its dependencies. Both problems share a hidden structure. There's a directed graph where an edge means " must come before ." No cycle of dependencies exists. Topological sort is the algorithm that turns this dependency graph into a single linear order. That order respects every constraint simultaneously. This ordering problem appears constantly in real systems. Build tools like `make` and Bazel topologically sort compilation units. Spreadsheet engines sort cell dependencies before recomputing formulas. Package managers like `npm` and `pip` resolve install order this way. Task schedulers in workflow engines (Airflow, Makefile-based CI pipelines) rely on topological order. This order decides which jobs can run only after their predecessors finish. Even compilers use it to order class initialization. They also use it to sequence instructions respecting data dependencies. The elegant surprise is that topological sort is not a new algorithm at all. It is Depth-First Search wearing a different hat. Once you understand DFS Edge Classification and Timestamps, topological sort falls out almost for free. Run DFS, and record when each vertex finishes exploring all its descendants. Then output vertices in decreasing order of finish time. The entire correctness argument reduces to one clean fact about DAGs. A directed graph has a valid linear ordering if and only if it has no cycle. DFS timestamps detect cycles exactly through the presence of "back edges." This article develops topological sort from that DFS foundation. It covers the precise definition of a topological order, the finish-time algorithm, and its pseudocode. It includes a rigorous correctness proof tied to the DFS edge classification you already know. It also offers a fully worked trace on a small dependency graph. Finally, it addresses the classic pitfalls: cyclic graphs, disconnected components, and confusing "order visited" with "order finished." It closes by connecting topological sort forward to the Single-Source Shortest Paths Framework. There, DAG shortest paths use exactly this ordering to relax edges in one linear pass.
- A topological order of a directed graph is a linear arrangement of vertices such that every edge has before .
- Such an order exists if and only if the graph is a DAG (directed, acyclic).
- The algorithm: run DFS, and output vertices in decreasing order of finish time.
- Correctness rests on one fact: in a DAG, DFS produces no back edges, so every edge goes from a later-finishing vertex to an earlier-finishing one.
- Runtime is , exactly the cost of one DFS traversal, plus to reverse/output the finish-time list.
- It underpins build systems, package managers, spreadsheet recomputation, and DAG shortest-path algorithms.
Topological sort is just DFS finish times read backwards — a vertex finishes after all its dependents, so decreasing finish order is a valid dependency order.
The idea
A directed acyclic graph (DAG) is a directed graph with no directed cycles. A topological order (or topological sort) of a DAG is a total ordering of the vertices such that for every edge , appears before in the ordering. Equivalently, it is any linear extension of the partial order induced by reachability in . Topological order is generally not unique. If a DAG has vertices with no edges between them, they can appear in either relative order. What is invariant is the constraint structure: whichever valid order you produce, every dependency edge must point forward. The key theorem connecting this to DFS is: > A directed graph has a topological order if and only if is acyclic. One direction is obvious: if has a cycle , no linear order can place before , ..., before , before — a contradiction. The other direction is where DFS earns its keep: it gives a constructive method to build a topological order whenever no cycle exists, and — crucially — a way to detect that no ordering exists when a cycle does. Recall from DFS Edge Classification and Timestamps that every edge explored by DFS on a directed graph falls into exactly one of four categories, determined by the discovery time and finish time of each vertex:
- Tree edge: leads to an undiscovered vertex (part of the DFS forest).
- Back edge: leads to an ancestor in the current DFS tree (the fingerprint of a cycle).
- Forward edge: leads to a proper descendant already finished.
- Cross edge: leads to a vertex in a different subtree or a previously finished tree.
The central theorem tying DFS to cycle detection is: > A directed graph is acyclic if and only if a DFS of produces no back edges. Topological sort combines these two facts: run DFS (which, on a DAG, produces only tree, forward, and cross edges), record finish times, and output vertices from largest to smallest finish time. The proof below shows this order satisfies every edge constraint.
How it works
The mechanism is deliberately minimal — it is standard DFS with one addition: whenever a vertex finishes (all its outgoing edges explored), push it onto the front of a result list (or append to a list that gets reversed at the end). ``` TOPOLOGICAL-SORT(G): for each vertex u in G.V: u.color = WHITE u.pi = NIL time = 0 L = empty list // will hold the topological order for each vertex u in G.V: if u.color == WHITE: DFS-VISIT(G, u, L) return L // vertices in topological order DFS-VISIT(G, u, L): time = time + 1 u.d = time // discovery time u.color = GRAY for each v in G.Adj[u]: if v.color == WHITE: v.pi = u DFS-VISIT(G, v, L) else if v.color == GRAY: error "cycle detected: graph is not a DAG" // GRAY case above is a back edge; BLACK v is forward/cross, ignore u.color = BLACK time = time + 1 u.f = time // finish time L.prepend(u) // insert u at the FRONT of L ``` The only new step relative to plain DFS is `L.prepend(u)` at the moment `u` is finished (colored BLACK). Because each vertex is prepended to the front of the list exactly when it finishes, the list ends up ordered from latest finish time to earliest — which is precisely the order the algorithm must output. An equivalent, and often more common, implementation appends finish times to a stack and pops the stack at the end, or appends to a list and reverses it once at the end: ``` TOPOLOGICAL-SORT-ALT(G): run DFS(G), recording finish time f[u] for every u sort vertices by f[u] in decreasing order return that ordering ``` In practice one never actually sorts by finish time explicitly — the recursive structure of DFS already emits vertices in exactly the right order if you push each vertex onto a stack the moment it finishes.
Notice in the figure that `Algo` — which depends on `LinAlg`, `Stats`, and `Prog2` — finishes first among its "in-degree neighbors" from the DFS root chosen last, yet still lands at the end of the topological order, because it has the smallest finish time overall. This is exactly the subtlety the correctness proof addresses.
Complexity & correctness
Time complexity: . The algorithm is unmodified DFS with extra work (a list prepend) per finished vertex. DFS itself visits every vertex once ( vertex initializations and colorings) and examines every edge at most once ( across all adjacency-list scans), giving total. This is asymptotically optimal: any correct algorithm must at least read the entire input (every vertex and edge), so is a lower bound, and the algorithm matches it.
Space complexity: for the color/discovery/finish arrays, the recursion stack (or explicit stack in an iterative version), and the output list, plus for the adjacency-list representation of the input itself (not extra working space).
Correctness proof. The claim to prove is: > If is a DAG, the list produced by decreasing finish time is a valid topological order — for every edge , precedes in , i.e. . Proof. Fix an edge . Consider the moment DFS first discovers (colors it GRAY). There are three cases for 's color at that instant: 1. is WHITE. Then the edge is explored and becomes a tree edge (or DFS recurses into via this exact call). By the parenthesis structure of DFS — the fact that the intervals for any two vertices are either nested or disjoint — 's whole DFS visit (discovery through finish) is nested strictly inside 's visit interval, because DFS-VISIT does not return until the recursive call on (and everything it spawns) completes. Hence . 2. is GRAY. Then is an ancestor of in the current DFS tree, so is a back edge. But is a DAG by hypothesis, and the standard DFS cycle-detection theorem (from DFS Edge Classification and Timestamps) says a directed graph has a back edge in some DFS if and only if it has a cycle. A back edge here, together with the tree path from down to , produces a directed cycle — contradicting acyclicity. So this case cannot occur. 3. is BLACK. Then has already finished, meaning was already set — and it was set before finishes (indeed before was even necessarily discovered, or at least before 's current exploration point). Since has not yet finished (we are still inside DFS-VISIT, which hasn't reached its own finishing step), will be assigned at some later time value than 's already-fixed value. So . In every possible (non-error) case, . Since lists vertices in decreasing finish-time order, appears before in whenever . This holds for every edge simultaneously, so is a valid topological order. The proof is airtight because case 2 is ruled out by acyclicity — this is the one place the DAG hypothesis is used, and it is exactly why the algorithm doubles as a cycle detector: if DFS ever encounters a GRAY neighbor, the graph is not a DAG, and no topological order exists. A correct implementation must check for this and abort/report failure rather than silently returning a bogus order.
Tradeoffs. Topological sort via DFS is simple and asymptotically optimal, but it is not unique (many valid orders may exist) and does not itself find the "best" or "canonical" order under secondary criteria (e.g., lexicographically smallest — Kahn's algorithm with a priority queue is preferred there, at ). The DFS-based method is also inherently recursive, so on extremely deep DAGs it may need an explicit stack to avoid recursion-depth limits in some languages.
Worked example
Consider a small build system: source files with `#include` dependencies, where an edge means " includes , so must compile first." Suppose the dependency graph is: ``` main.c -> util.c main.c -> parser.c parser.c -> lexer.c parser.c -> util.c lexer.c -> util.c ``` Adjacency lists (iterated in this listed order): ``` main.c: [util.c, parser.c] util.c: [] parser.c: [lexer.c, util.c] lexer.c: [util.c] ``` Run DFS starting from `main.c` (first vertex in the outer loop):
| Step | Action | d[·] | f[·] | Notes |
|---|---|---|---|---|
| 1 | Discover `main.c` | d=1 | — | color GRAY |
| 2 | Visit edge `main.c → util.c`, `util.c` WHITE → recurse | d=2 | — | GRAY |
| 3 | `util.c` has no outgoing edges → finish | — | f=3 | BLACK, prepend to L |
| 4 | Back in `main.c`, visit edge `main.c → parser.c`, WHITE → recurse | d=4 | — | GRAY |
| 5 | Visit edge `parser.c → lexer.c`, WHITE → recurse | d=5 | — | GRAY |
| 6 | Visit edge `lexer.c → util.c`, `util.c` is BLACK (already finished) → cross edge, skip | — | — | no action |
| 7 | `lexer.c` has no more edges → finish | — | f=6 | BLACK, prepend to L |
| 8 | Back in `parser.c`, visit edge `parser.c → util.c`, BLACK → cross edge, skip | — | — | no action |
| 9 | `parser.c` has no more edges → finish | — | f=7 | BLACK, prepend to L |
| 10 | Back in `main.c`, no more edges → finish | — | f=8 | BLACK, prepend to L |
Resulting finish times: `util.c`=3, `lexer.c`=6, `parser.c`=7, `main.c`=8. Reading off in decreasing finish-time order:
Check every edge points forward in this list:
- `main.c → util.c`: main.c is position 1, util.c is position 4. ✓
- `main.c → parser.c`: position 1 before position 2. ✓
- `parser.c → lexer.c`: position 2 before position 3. ✓
- `parser.c → util.c`: position 2 before position 4. ✓
- `lexer.c → util.c`: position 3 before position 4. ✓
Every dependency is respected: `util.c` compiles first (it depends on nothing), then `lexer.c`, then `parser.c`, then `main.c` — this exactly matches intuition, since `main.c` is the top-level file depending (directly or transitively) on everything else, and the sort correctly places it last, i.e., "compile last." Notice that `util.c` was discovered second but finishes first — it is a leaf with no dependencies, so DFS immediately closes it out. This is the crux of why topological order uses finish time, not discovery time: discovery order here would wrongly suggest `util.c` should come right after `main.c`, which happens to still work in this instance but is not the theorem being used — decreasing finish time is the property that is provably always correct.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Using discovery time instead of finish time | Feels intuitive ("visited first = comes first") but is not correct in general — a vertex can be discovered early yet have descendants that must precede others discovered later | Always sort by decreasing finish time, proven correct via the parenthesis theorem |
| Forgetting to check for back edges | Running plain DFS on a graph that isn't actually a DAG silently "succeeds" and produces a bogus ordering | Explicitly test neighbor color; if GRAY, report "not a DAG" instead of returning an order |
| Ignoring disconnected components | Only calling DFS-VISIT from one start vertex misses unreachable vertices entirely | Outer loop must call DFS-VISIT on every WHITE vertex, not just one root |
| Confusing "a" topological order with "the" topological order | Topological order is generally non-unique; comparing your output against someone else's and calling it wrong | Verify by checking every edge points forward, not by string-matching a specific sequence |
| Off-by-one in list insertion | Appending to the end of the list instead of prepending (or forgetting to reverse) yields the reverse of the correct order | Prepend on finish, or append and reverse once at the very end |
| Treating a topological order of a general digraph as always existing | Attempting topological sort on graphs with cycles (e.g., mutual imports, circular task dependencies) | Detect the cycle via the GRAY-neighbor check and report the specific cycle if possible, rather than crashing or looping |
Pioneers & history
Topological sorting as a combinatorial concept predates modern computer science — it is a special case of finding a linear extension of a partial order, studied in order theory throughout the 20th century. Its algorithmic formulation using DFS, however, is inseparable from the broader development of systematic graph-traversal techniques for digraphs, brought to computer science largely through the work on structured algorithm design and complexity analysis in the 1970s.
Origins · 1 minJohn E. Hopcroft and how this was discovered
Where this matters
Build systems and compilers. Tools such as `make`, `ninja`, Bazel, and Gradle model source files, targets, and libraries as a dependency DAG; topological sort determines a valid compilation/linking order, and cycle detection catches circular dependency bugs before a build even starts.
Package and module managers. `npm`, `pip`, `apt`, and language module resolvers (Python imports, Java class loading) topologically order packages so that every dependency is installed or initialized before anything that needs it.
Spreadsheet and dataflow engines. Formula-dependency graphs in spreadsheets, and dataflow graphs in systems like Apache Spark or TensorFlow's static graph mode, are topologically sorted to decide a legal evaluation order that guarantees every input is computed before it is consumed.
Workflow orchestration. Systems like Apache Airflow, Argo Workflows, and CI/CD pipelines (GitHub Actions, GitLab CI) represent job dependencies as a DAG and use topological order to schedule jobs, often parallelizing independent branches of the order.
Course/task scheduling. University degree-audit systems and generic task schedulers with prerequisite constraints use exactly this algorithm to validate and generate feasible sequences.
Algorithmic foundation for further graph theory. Topological sort is the entry point to DAG-specific optimizations: single-source shortest paths on a DAG can be solved in one linear pass by relaxing edges in topological order — the very first specialization covered in the Single-Source Shortest Paths Framework — and critical-path analysis in project scheduling (PERT/CPM) is a direct application of this same DAG shortest/longest-path technique.
Ask the tutor: How does the topological order computed here get used to solve single-source shortest paths on a DAG in a single linear pass?
Is the topological order of a DAG always unique?
No. Uniqueness only occurs when the DAG's underlying partial order is a total order — that is, every pair of vertices has a directed path between them in one direction. In general, if two vertices have no path connecting them either way (they're incomparable in the partial order), they can appear in either relative order in a valid topological sort. For example, in a graph with edges `a→c` and `b→c` only, both `a,b,c` and `b,a,c` are valid topological orders. DFS-based sort returns exactly one such valid order, determined by the outer loop's vertex iteration order and each vertex's adjacency-list order — different implementations, or the same implementation on differently-ordered adjacency lists, can produce different (equally valid) outputs.
Why does topological sort use finish time and not discovery time?
Discovery time reflects when DFS first reaches a vertex, but a vertex discovered early can have a huge subtree of descendants that must all be processed — and, crucially, must all appear after it — before DFS returns to finish it. Finish time captures "all my descendants are done," which is exactly the guarantee needed: the correctness proof shows that for every edge , always holds in a DAG, via the DFS parenthesis (nesting) theorem. No such universal inequality holds for discovery times — a vertex can be discovered before another vertex that must nonetheless precede it in any valid order (e.g., a sibling subtree explored later but structurally independent).
Can topological sort be done without recursion or DFS at all?
Yes — Kahn's algorithm is a popular alternative. It repeatedly removes vertices with in-degree zero, appends them to the output, and decrements the in-degree of their neighbors, using a queue to track newly-zero-in-degree vertices. It runs in the same time, avoids recursion (useful for very deep graphs or languages with small call stacks), and naturally detects cycles: if the algorithm terminates having output fewer than vertices, a cycle exists among the remainder. Kahn's algorithm is often preferred in production build systems because it processes vertices in "readiness" order, which maps naturally onto parallel task scheduling — you can dispatch every currently-zero-in-degree vertex concurrently.
How do you actually detect and report a cycle, not just fail?
During DFS, maintain the recursion stack (or equivalently the set of GRAY vertices). When you find an edge to a GRAY vertex from the current vertex , you've found a back edge, and the path from down to in the current DFS tree, plus the edge , forms an explicit cycle. By keeping parent pointers () recorded during DFS, you can walk back from through pointers until you reach , reconstructing the exact cycle to report to the user (e.g., "circular import: a.py → b.py → c.py → a.py"). This is far more useful in practice than a bare "graph is not a DAG" error.
Does topological sort work on undirected graphs?
No — topological order is only meaningful for directed graphs, because the whole point is establishing a "must come before" relation, which requires edge direction. Running the DFS edge-classification machinery on an undirected graph produces only tree edges and back edges (no forward or cross edges, by a related DFS theorem for undirected graphs), and any edge at all, traversed in either direction, would create a symmetric constraint — meaning no acyclic undirected graph with at least one edge could ever have both endpoints "before" each other. The concept simply doesn't apply; the analogous undirected notion is a spanning tree or a DFS/BFS traversal order, not a topological sort.
How does topological sort relate to strongly connected components (SCCs)?
If a directed graph has cycles, it has no topological order — but you can still extract useful structure by first condensing each strongly connected component into a single "supernode." The resulting condensation graph is always a DAG (this is a theorem in its own right), and that graph can be topologically sorted. This combination — SCC decomposition followed by topological sort of the condensation — is the backbone of Tarjan's and Kosaraju's SCC algorithms, and it generalizes topological sort to arbitrary directed graphs by first isolating the "acyclic skeleton" of the graph's structure.
What real bug does a missing cycle check cause in production?
A classic case: a spreadsheet or build system with a genuine circular dependency (A depends on B, B depends on A) is processed by an unguarded DFS-based topological sort. The DFS still terminates — it just silently omits the correctness guarantee, since the "GRAY neighbor" case was never checked. The system then evaluates cells or compiles files using stale or partially-computed values from a previous run, producing subtly wrong output rather than a crash. This is strictly worse than a hard failure, because it can go unnoticed for a long time; production topological-sort implementations should always treat "cycle detected" as a first-class, tested code path, not a rare edge case.