~/graphs/topological-sort
Topological sort
Put the nodes of a directed graph in an order where every edge points forward, or find out that a cycle makes it impossible.
Repeatedly take a node that nothing is still waiting on, output it, and delete its outgoing edges.
Tasks with "must come before" rules: course prerequisites, build steps, spreadsheet cells, package installs.
O(V + E)
O(V + E)
You’ll recognise it when
- The input is a list of “a must come before b” rules: prerequisites, dependencies, build steps, recipes that need other recipes.
- You’re asked for any valid order, or whether one exists at all (“can you finish all courses?”).
- Something must be processed only after everything it depends on: spreadsheet cells, package installs, DP over a directed graph.
- The question hints at a cycle being the reason an answer might not exist.
It’s easy to mistake for plain BFS, since Kahn’s algorithm also uses a queue. The difference is when a node goes in: BFS adds it the first time it’s seen, topological sort only once all of its incoming edges are gone.
The idea
Think of getting dressed. Socks before shoes, underwear before trousers, trousers before shoes, shirt before tie. You don’t need the whole plan up front: at any moment, put on something whose requirements are all already on you. Socks are free from the start. Shoes become free only once both socks and trousers are on.
That’s the whole algorithm. A topological order lists the nodes of a directed graph so that every edge u → v has u earlier than v. Keep, for each node, a count of how many of its prerequisites are still missing: its indegree. A node with indegree 0 is free to go next. Placing it deletes its outgoing edges, which may free more nodes.
A cycle makes an order impossible. If a → b → c → a, then a must come before c and c before a. Kahn’s algorithm finds this by itself: the nodes on a cycle wait on each other forever, so they never reach indegree 0 and never come out.
How it works
The input is n nodes numbered from 0 and a list of edges (u, v) meaning u before v. The graphic follows the steps as you scroll; the example is eight courses and their prerequisites.
- Count the incoming edges of every node.
indegree[v]is how many nodes must come beforev. - Every node with
indegree0 can go first. Put them all in a FIFOqueue, smallest id first. - Take the node at the front of the
queueand append it toorder. - Remove each of its outgoing edges
u → vby doingindegree[v] -= 1. You never delete anything from the graph; the counter does the work. - If
indegree[v]just hit 0, everything beforevis placed, sovjoins the back of thequeue. - A node with several prerequisites waits: node 6 drops from 2 to 1 and stays out until its last edge goes.
- When the
queueis empty, check the length. Allnnodes inordermeans it’s a valid topological order. Fewer means the rest are stuck behind a cycle.
Why it’s correct: a node joins the queue only when every edge into it has been removed, and an edge is removed only when its start node is placed. So every node comes after all of its prerequisites. And if the graph has no cycle, some unplaced node always has indegree 0 (follow incoming edges backwards among unplaced nodes: without a cycle you must hit a start), so nothing gets left behind.
Here the same code runs into a cycle. After 0 and 5 are placed, nodes 1, 2 and 3 each wait on another, and 4 waits on 3:
The order this code produces is fixed by two choices: sources start in increasing id, and each node’s edges are processed in input order. Many valid orders usually exist; this is one of them.
from collections import deque
def topo_sort(n, edges):
"""Kahn's algorithm. Each edge (u, v) means u must come before v.
Returns an order of the nodes 0..n-1, or [] if there is a cycle."""
adj = [[] for _ in range(n)]
indegree = [0] * n
for u, v in edges:
adj[u].append(v)
indegree[v] += 1
queue = deque(v for v in range(n) if indegree[v] == 0)
order = []
while queue:
u = queue.popleft()
order.append(u)
for v in adj[u]: # "remove" the edge u -> v
indegree[v] -= 1
if indegree[v] == 0:
queue.append(v)
# Nodes left over never reached indegree 0: they sit on or after a cycle.
if len(order) < n:
return []
return order#include <queue>
#include <utility>
#include <vector>
// Kahn's algorithm. Each edge {u, v} means u must come before v.
// Returns an order of the nodes 0..n-1, or an empty vector if there is a cycle.
std::vector<int> topoSort(int n, const std::vector<std::pair<int, int>>& edges) {
std::vector<std::vector<int>> adj(n);
std::vector<int> indegree(n, 0);
for (auto [u, v] : edges) {
adj[u].push_back(v);
indegree[v]++;
}
std::queue<int> queue;
for (int v = 0; v < n; v++) {
if (indegree[v] == 0) queue.push(v);
}
std::vector<int> order;
while (!queue.empty()) {
int u = queue.front();
queue.pop();
order.push_back(u);
for (int v : adj[u]) { // "remove" the edge u -> v
indegree[v]--;
if (indegree[v] == 0) queue.push(v);
}
}
// Nodes left over never reached indegree 0: they sit on or after a cycle.
if ((int)order.size() < n) return {};
return order;
}import java.util.*;
class TopoSort {
// Kahn's algorithm. Each edge {u, v} means u must come before v.
// Returns an order of the nodes 0..n-1, or an empty array if there is a cycle.
static int[] sort(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
int[] indegree = new int[n];
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
indegree[e[1]]++;
}
ArrayDeque<Integer> queue = new ArrayDeque<>();
for (int v = 0; v < n; v++) {
if (indegree[v] == 0) queue.add(v);
}
int[] order = new int[n];
int size = 0;
while (!queue.isEmpty()) {
int u = queue.poll();
order[size++] = u;
for (int v : adj.get(u)) { // "remove" the edge u -> v
indegree[v]--;
if (indegree[v] == 0) queue.add(v);
}
}
// Nodes left over never reached indegree 0: they sit on or after a cycle.
if (size < n) return new int[0];
return order;
}
}Why it’s O(V + E)
Each node enters and leaves the queue at most once: O(V). Each edge is looked at twice, once while counting indegree and once when it’s removed: O(E). Building the adjacency lists is O(V + E) too.
Space is the adjacency lists, O(V + E), plus indegree, the queue and order, each O(V).
If you need the smallest order in dictionary order, swap the queue for a min-heap. Each push and pop then costs O(log V), for O((V + E) log V) in total.
Common mistakes
Getting the edge direction backwards
“Course a has prerequisite b”, often written [a, b], means the edge goes from b to a. Flip it and you get the order exactly reversed, with no error to warn you.
for a, b in prereqs: adj[a].append(b); indegree[b] += 1 # ✗ a before b
for a, b in prereqs: adj[b].append(a); indegree[a] += 1 # ✓ b before a
Enqueueing a node the first time you see it
That’s BFS, not topological sort. A node with two prerequisites would go out after the first one, before the second.
if v not in seen: queue.append(v) # ✗ ignores other prerequisites
if indegree[v] == 0: queue.append(v) # ✓ waits for all of them
Returning the order without checking its length
When there’s a cycle, the loop just stops early. The partial order looks fine, but it’s missing every node on or behind the cycle.
return order # ✗ silently incomplete
return order if len(order) == n else [] # ✓ report the cycle
Only counting nodes that appear in an edge
If you build indegree from the edge list with a dictionary, nodes with no edges at all never get an entry, and nodes with no incoming edges may be missed too. Those are exactly the nodes that should start the queue. Give every node an indegree of 0 first.
Variations
- Course schedule. “Can you finish all courses?” is just the length check:
len(order) == n. “In what order?” returnsorderitself. - DFS-based topological sort. Run DFS and append each node after all its descendants finish. That post-order, reversed, is a topological order. To detect cycles, colour nodes white (unseen), grey (on the current path) and black (finished): an edge to a grey node closes a cycle.
- Longest path in a DAG. Walk the nodes in topological order and relax each outgoing edge with
best[v] = max(best[v], best[u] + w). Every node is final before it’s used, so this runs in O(V + E), even though longest path is hard on general graphs. - Parallel scheduling by levels. Instead of one node at a time, take the whole
queueas a round. Everything in a round can run at the same time, and the number of rounds is the minimum number of semesters (or build stages) needed. - Smallest order in dictionary order. Use a min-heap instead of the FIFO
queue, so the smallest ready node always goes next.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
You get
ncourses and pairs[a, b]meaning coursebmust be taken before coursea. Return an order that takes every course, or an empty list if that’s impossible. Which approach fits?The pairs are directed edges and a valid schedule is exactly a topological order; a short result means a cycle. BFS from one node adds a course as soon as it’s first seen, before its other prerequisites. Union-find forgets direction. Sorting by prerequisite count fails when a course with one prerequisite depends on a course with three.
-
2
What does this print?
from collections import dequeedges = [(3, 1), (0, 1), (0, 2), (1, 4), (2, 4)]n = 5adj = [[] for _ in range(n)]indegree = [0] * nfor u, v in edges:adj[u].append(v); indegree[v] += 1queue = deque(v for v in range(n) if indegree[v] == 0)order = []while queue:u = queue.popleft(); order.append(u)for v in adj[u]:indegree[v] -= 1if indegree[v] == 0: queue.append(v)print(order)Nodes 0 and 3 start with indegree 0, so the queue is
[0, 3]. Taking 0 drops node 1 to 1 (it still needs 3) and frees 2. Taking 3 then frees 1, which joins the queue behind 2. Then 2 and 1 each remove an edge into 4, and 4 comes last. The tempting[0, 3, 1, 2, 4]forgets that 1 wasn’t ready when 2 was queued. -
3
DFS-based topological sort appends each node after all of its neighbours finish, then reverses. What does this print?
g = {0: [1, 2], 1: [3], 2: [3], 3: []}seen, post = set(), []def dfs(u):seen.add(u)for v in g[u]:if v not in seen: dfs(v)post.append(u)for u in g:if u not in seen: dfs(u)print(post[::-1])The DFS from 0 goes to 1, then 3; 3 finishes first, then 1, then 2 (3 is already seen), then 0. Post-order is
[3, 1, 2, 0]and reversed it is[0, 2, 1, 3], a valid order. Printing the post-order without reversing ([3, 1, 2, 0]) puts every node before its prerequisites. -
4
A build tool runs Kahn’s algorithm over this in-degree table. What goes wrong?
indegree, adj = defaultdict(int), defaultdict(list)for target, deps in rules.items():for d in deps:adj[d].append(target)indegree[target] += 1queue = deque(t for t in indegree if indegree[t] == 0)indegreeonly gains keys when something is incremented, so the targets with no dependencies, the ones that should start the queue, are never in it.defaultdictonly creates a key when you look one up, and the loop never looks up those nodes. Setindegree[v] = 0for every node first. -
5
You must return the smallest valid topological order in dictionary order. What’s the right change, and what does it cost?
At every step you must pick the smallest node that is ready right now, which is what a min-heap gives you. Sorting the output afterwards breaks dependencies. Sorting adjacency lists or the initial sources only controls the order nodes join a FIFO queue, so a small node freed late still waits behind bigger ones queued earlier.
Practice problems
- easy Course Schedule (CSES) cses.fi
- medium Course Schedule leetcode.com
- medium Course Schedule II leetcode.com
- medium Find All Possible Recipes from Given Supplies leetcode.com
- medium Longest Flight Route cses.fi
- hard Parallel Courses III leetcode.com
- hard Sort Items by Groups Respecting Dependencies leetcode.com