🚀 Phases 1–5 are live — Days 1–17 cover the foundations and the algorithmic patterns. See the roadmap →
Day 23 - Topological SortKahn's Algorithm (BFS)

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 → 1

Initial indegrees: [2, 2, 1, 1, 0, 0] (for nodes 0..5).

StepQueueEmitOutputIndegrees 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

In Kahn's algorithm, why do we ONLY enqueue a node when its indegree DROPS to 0, not when it's < some_threshold?
To get the lexicographically smallest topo order, you replace the FIFO queue with:
On a graph with a cycle, what does Kahn's return?

Next: DFS topological sort — the recursive variant, post-order emission, and the elegance of a single DFS.