Kahn’s Algorithm
Kahn’s algorithm (1962) is the most intuitive topological sort. The idea is exactly what you’d do by hand:
Find a task with no remaining prerequisites. Do it. Cross it off everyone else’s prerequisite list. Repeat until everything is done — or until something with prerequisites remains, in which case there’s a cycle.
In graph language: find a node with indegree 0. Emit it. Decrement the indegree of every neighbor. If a neighbor’s indegree hits zero, it’s now eligible. Repeat.
It’s BFS in disguise — the queue holds “currently eligible” nodes, and we expand by emitting and decrementing.
The template
1. Build adjacency list adj and indegree[] from edges.
2. Initialize queue with every v where indegree[v] == 0.
3. While queue is nonempty:
u = queue.pop()
append u to output
for each edge u -> v:
indegree[v] -= 1
if indegree[v] == 0:
queue.push(v)
4. If len(output) == V, return output.
Else, the graph has a cycle.Four lines of pseudocode, all O(V + E).
The full code
Time: O(V + E). Space: O(V + E) for the adjacency list and indegree array.
Why it works (proof in two paragraphs)
Correctness: every node we emit has indegree 0 at the moment we emit it — meaning all its prerequisites are already in the output. So for every edge u → v, u appears before v: that’s the definition of a topological order.
Completeness on DAGs: if there’s no cycle, at least one node has indegree 0 at every step. Proof by contradiction: if every remaining node had indegree >= 1, you could trace edges backward from any node and never run out — but with finitely many nodes, you’d eventually revisit a node, forming a cycle. Contradiction. So we always have something to emit, and we emit every node exactly once.
Stepping through an example
Consider the graph with edges:
5 → 0, 5 → 2, 4 → 0, 4 → 1, 2 → 3, 3 → 1Initial indegrees: [2, 2, 1, 1, 0, 0] (for nodes 0..5).
| Step | Queue | Emit | Output | Indegrees changed |
|---|---|---|---|---|
| 0 | [4, 5] | [] | ||
| 1 | [5, 1] | 4 | [4] | 0 → 1, 1 → 1 (1’s hit 1, but not 0) |
| 2 | [1, 0, 2] | 5 | [4, 5] | 0 → 0, 2 → 0 (both became 0) |
| 3 | [0, 2] | 1 | [4, 5, 1] | no out-edges from 1 |
| 4 | [2] | 0 | [4, 5, 1, 0] | no out-edges from 0 |
| 5 | [3] | 2 | [4, 5, 1, 0, 2] | 3 → 0 |
| 6 | [] | 3 | [4, 5, 1, 0, 2, 3] | no out-edges from 3 |
Final output: [4, 5, 1, 0, 2, 3] — a valid topo order. Other orders are equally valid; which one comes out depends on the order we initially push zero-indegree nodes.
The output depends on the queue’s behavior. A FIFO queue gives one order; a stack (LIFO) gives a different one; a priority queue gives the smallest available eligible node first. All produce valid topo orders if the graph is a DAG.
Variation 1 — Lexicographically smallest topo order
Replace the FIFO queue with a min-heap. When multiple nodes have indegree 0, the heap returns the smallest one first.
Time: O((V + E) log V) because of the heap operations.
This is the trick behind Alien Dictionary when the problem wants a deterministic answer.
Variation 2 — Parallel scheduling depth
Tasks have prerequisites. If we can run any number of tasks in parallel as long as their prerequisites are done, what’s the minimum number of time-steps to finish all tasks?
The answer is the length of the longest path in the DAG. Equivalently, the number of “rounds” in a level-by-level Kahn’s:
- Round 0: all nodes with indegree 0.
- Round 1: all nodes whose indegree became 0 after round 0 emissions.
- …
Tweak Kahn’s to track levels:
Same O(V + E) as plain Kahn’s, plus a per-round bookkeeping. This is Parallel Courses I on LeetCode.
Variation 3 — Count distinct topo orders
Multiple valid orders may exist. Counting them is #P-hard in general (no polynomial algorithm known unless P = NP). For small graphs, you can enumerate via backtracking over “which zero-indegree node to pick next.”
Common bugs
The “decrement-then-check” idiom. Use if (--indeg[v] == 0) (C++/Java) or indeg[v] -= 1; if indeg[v] == 0 (Python). Checking before decrementing is wrong; decrementing twice is wrong. The single-step decrement-and-check is what makes the algorithm O(V + E) — every edge contributes one decrement, total O(E).
Forgetting to check len(output) == V. Without it, on a cyclic input you’d return a partial order — looks valid for the prefix, silently wrong overall. The length check is the cycle detector; never skip it.
Edge direction. [a, b] in LeetCode’s Course Schedule means “to take a, you need b first,” so the edge is b → a. Read the problem statement carefully — half of “Course Schedule wrong answer” bugs come from flipping this direction.
Quick check
Next: DFS topological sort — the recursive variant, post-order emission, and the elegance of a single DFS.