Topological Sort
Overviewโ
A topological sort orders the vertices of a directed acyclic graph so that every edge points forward โ if A must happen before B, A appears earlier. It is the algorithm behind build systems, package managers, task schedulers and spreadsheet recalculation, all of which face the same question: given these dependencies, what order can I do the work in?

Core Conceptsโ
| Term | Meaning |
|---|---|
| DAG | Directed acyclic graph โ the precondition. A cycle makes ordering impossible |
| In-degree | Number of incoming edges; a vertex with in-degree 0 has no unmet prerequisites |
| Valid orderings | Usually many. The algorithms find a valid order, not the order |
If the graph has a cycle, no valid ordering exists, and both algorithms below detect it. That detection is often the more useful output: "circular dependency between A, B and C" is precisely what a package manager or build tool needs to report.
Architecture / Mechanismโ
Kahn's algorithm (BFS-based)โ
Repeatedly take a vertex with no remaining prerequisites, output it, and remove its edges:
from collections import deque
def topological_sort(graph):
"""graph: {node: [dependents...]}, an edge u -> v meaning u must come before v."""
in_degree = {v: 0 for v in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
queue = deque(v for v, d in in_degree.items() if d == 0)
order = []
while queue:
u = queue.popleft()
order.append(u)
for v in graph[u]:
in_degree[v] -= 1 # u is done; one prerequisite satisfied
if in_degree[v] == 0:
queue.append(v)
if len(order) != len(graph): # some vertices never reached in-degree 0
raise ValueError("graph contains a cycle")
return order
The final length check is the cycle detection: vertices inside a cycle always have at least one unsatisfied prerequisite, so they never enter the queue.
DFS-basedโ
Run depth-first search and prepend each vertex as it finishes. A vertex finishes only after everything it depends on has, so the reversed finishing order is a topological order:
def topological_sort_dfs(graph):
WHITE, GREY, BLACK = 0, 1, 2
colour = {v: WHITE for v in graph}
order = []
def visit(u):
if colour[u] == GREY:
raise ValueError("graph contains a cycle")
if colour[u] == BLACK:
return
colour[u] = GREY
for v in graph[u]:
visit(v)
colour[u] = BLACK
order.append(u) # post-order: appended after all descendants
for v in graph:
visit(v)
return order[::-1] # reverse the finishing order
| Kahn's | DFS-based | |
|---|---|---|
| Traversal | Breadth-first | Depth-first |
| Cycle detection | Output shorter than vertex count | A GREY vertex revisited |
| Recursion | None | Yes โ stack depth O(V) |
| Ordering control | Swap the queue for a heap to get a deterministic or prioritised order | Fixed by the traversal |
| Natural extension | Level-by-level parallel scheduling | โ |
Kahn's is usually preferable: it is iterative, its cycle detection is a single comparison, and it extends naturally to the parallel case below.
Practical Usageโ
# Everything at the same "level" has no dependencies between its members,
# so each level can be executed in parallel.
def parallel_batches(graph):
in_degree = {v: 0 for v in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
ready = [v for v, d in in_degree.items() if d == 0]
batches = []
while ready:
batches.append(ready) # this whole batch can run concurrently
nxt = []
for u in ready:
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
nxt.append(v)
ready = nxt
return batches
This is what make -j and modern build systems do: compute the dependency levels, then run each
level's tasks in parallel. The number of batches is the graph's critical path length, and it is a
hard floor on build time no matter how many cores you add โ the same argument as
Amdahl's law.
| Domain | Vertices | Edge means |
|---|---|---|
Build systems (make, Bazel) | Targets | "must be built before" |
Package managers (apt, pip, cargo) | Packages | "depends on" |
| Spreadsheets | Cells | "is referenced by" |
| Course planning | Courses | "is a prerequisite for" |
| Task runners, CI pipelines | Jobs | "must complete before" |
| Compilers | Instructions | Data dependency โ used for scheduling |
Edge Cases & Pitfallsโ
- Edge direction is the most common bug. Decide whether your map means "depends on" or "is depended on by" โ reversing it produces a perfectly plausible, exactly backwards order.
- Isolated vertices belong in the output. A package with no dependencies still needs installing;
make sure it is initialised in
in_degree. - Vertices appearing only as targets may be missing from
graph's keys, causing aKeyError. Build the vertex set from both endpoints of every edge. - The result is not unique. Tests asserting one specific order will break when the iteration order changes. Assert the constraints โ that each vertex precedes its dependents โ or use a heap for a deterministic order.
- DFS recursion depth is O(V); use Kahn's for large graphs.
Referencesโ
- Kahn, A.B. (1962), "Topological sorting of large networks", Communications of the ACM โ the original.
- Cormen, Leiserson, Rivest & Stein, Introduction to Algorithms, ยง22.4 โ the DFS-based version, with the proof that reverse finishing order is a valid ordering.
Books & Videosโ
- VisuAlgo โ Topological Sort โ both algorithms, on a graph you can edit.
Related Pagesโ
- Traversal: BFS & DFS โ both algorithms are traversals with extra bookkeeping.
- Graphs โ DAGs and why acyclicity matters.
- Multicore & Parallelism โ the critical-path limit on parallel builds.