Kahn’s Algorithm (BFS-based Topological Sort)
Purpose: Produce a linear ordering of vertices in a Directed Acyclic Graph (DAG) such that every edge goes from an earlier vertex to a later one, in O(V + E) time.
Algorithm
- Compute the in-degree (number of incoming edges) of every vertex.
- Push all vertices with in-degree 0 into a queue — these have no dependencies and can safely go first.
- Repeatedly pop a vertex from the queue and add it to the topological order.
- For each neighbor of the popped vertex, decrement its in-degree by 1 (since one of its dependencies is now resolved).
- If a neighbor’s in-degree drops to 0, push it into the queue.
- Repeat until the queue is empty. If the resulting order contains all
Vvertices, it’s a valid topological sort; if fewer, the graph has a cycle.
Code
vector<int> kahnTopoSort(int n, vector<vector<int>>& adj) {
vector<int> indegree(n, 0);
for (int u = 0; u < n; u++)
for (int v : adj[u]) indegree[v]++;
queue<int> q;
for (int i = 0; i < n; i++) if (indegree[i] == 0) q.push(i);
vector<int> order;
while (!q.empty()) {
int u = q.front(); q.pop();
order.push_back(u);
for (int v : adj[u]) {
if (--indegree[v] == 0) q.push(v);
}
}
if ((int)order.size() != n) return {}; // cycle detected
return order;
}Paradigm
Greedy. At every step it picks any vertex with zero remaining dependencies and commits to placing it next, relying on the fact that a vertex with in-degree 0 can always safely go before all its remaining neighbors without violating any edge constraint.
Complexity
- Time: O(V + E)
- Space: O(V)
Proof of Correctness
Claim: The order produced is a valid topological order, and the algorithm processes all V vertices if and only if the graph is acyclic.
Why the order respects all edges: A vertex v is only pushed into the queue after every one of its in-edges has been “resolved” (its predecessor already placed in order, causing v’s in-degree to drop to 0). So by construction, every predecessor of v appears in order strictly before v, meaning every edge u → v has u appearing before v. This holds for every vertex processed, so the final order respects all edges.
Why every vertex is eventually processed (if the graph is acyclic): Suppose the graph is acyclic but some vertex x is never added to the queue. Since the graph is a DAG, x has some finite set of ancestors, tracing back must terminate at a vertex with in-degree 0 (a DAG always has at least one source). That source gets processed first, decrementing its neighbors’ in-degrees, eventually propagating down every dependency chain leading to x. Since there’s no cycle to create a circular wait, every ancestor of x is eventually processed, so x’s in-degree eventually reaches 0 and it gets pushed — contradiction. So all vertices are processed.
Why a cycle causes fewer than V vertices to be output: Every vertex in a cycle depends on another vertex in the same cycle, so none of them can ever reach in-degree 0 (each one’s last required predecessor is inside the cycle itself, which never gets processed first). So cycle vertices are never pushed into the queue, and order.size() < n correctly flags this. ∎
Variants / Use Cases
- DFS-based topological sort → alternative approach using post-order DFS and reversing the finish order, doesn’t need explicit in-degree tracking
- Cycle detection in directed graphs → a direct byproduct: if Kahn’s can’t process all vertices, the graph has a cycle
- Course scheduling / build systems → classic real-world application where tasks have prerequisite dependencies
- Parallel task scheduling → vertices with in-degree 0 at any point can be processed simultaneously, useful for parallelizing independent tasks
- Lexicographically smallest topological order → replace the queue with a min-heap to always pick the smallest available vertex next
Topological Sort (DFS-based)
Purpose: Produce a linear ordering of vertices in a Directed Acyclic Graph (DAG) such that every edge goes from an earlier vertex to a later one, in O(V + E) time using DFS finish order.
Algorithm
- Maintain a
visitedarray and an empty stack (or list) to record finish order. - Run DFS from every unvisited vertex.
- In the DFS, recurse into all unvisited neighbors first before doing anything else.
- Once all neighbors of a vertex are fully explored, push that vertex onto the stack (it “finishes”).
- After DFS completes from all starting points, pop everything off the stack — this gives the topological order.
- (Cycle check, if needed) If a DFS ever revisits a vertex that’s still “in progress” (on the current recursion path, not yet finished), the graph has a cycle and no valid topological order exists.
Code
void dfsTopo(int u, vector<vector<int>>& adj, vector<bool>& visited, stack<int>& finishOrder) {
visited[u] = true;
for (int v : adj[u]) {
if (!visited[v]) dfsTopo(v, adj, visited, finishOrder);
}
finishOrder.push(u);
}
vector<int> dfsTopoSort(int n, vector<vector<int>>& adj) {
vector<bool> visited(n, false);
stack<int> finishOrder;
for (int i = 0; i < n; i++) {
if (!visited[i]) dfsTopo(i, adj, visited, finishOrder);
}
vector<int> order;
while (!finishOrder.empty()) {
order.push_back(finishOrder.top());
finishOrder.pop();
}
return order;
}Paradigm
Decrease and Conquer. Each DFS call fully resolves a vertex’s entire dependent subtree before finishing that vertex itself, recursively reducing the ordering problem to smaller unresolved subgraphs until the base case (a vertex with no unvisited neighbors) is hit.
Complexity
- Time: O(V + E)
- Space: O(V)
Proof of Correctness
Claim: For every directed edge u → v, u finishes after v in the DFS — so reversing finish order places u before v, giving a valid topological order.
Proof: Consider any edge u → v in a DAG. When DFS visits u and looks at this edge, one of two cases holds:
vis unvisited: DFS recurses intovimmediately, and since DFS fully completes a recursive call before returning,v(and everything reachable from it) finishes before control returns tou. Sovfinishes beforeu.vis already visited: since the graph has no cycles,vcannot be an ancestor ofuin the current DFS path (that would create a cycle via the edgeu → v). Sovmust have already finished in a previous, unrelated DFS branch — meaningvfinished beforeueven started this edge check.
In both cases, v finishes before u. Since finish order is pushed onto a stack, popping the stack (reverse finish order) places u before v — exactly satisfying the edge direction requirement. This holds for every edge, so the final order is a valid topological sort.
Why this only works on a DAG: If a cycle exists, some vertex v would need to finish before an ancestor u on its own cycle, while also requiring u to finish before v (going the other way around the cycle) — a contradiction, which is exactly why cycles break topological ordering. ∎
Variants / Use Cases
- Kahn’s Algorithm (BFS-based) → alternative approach using in-degree tracking and a queue, doesn’t require recursion or a stack
- Cycle detection in directed graphs → track a “currently in recursion stack” state; revisiting such a vertex confirms a cycle
- Course scheduling / build systems → classic real-world application where tasks have prerequisite dependencies
- Dependency resolution (package managers, compilers) → DFS-based order is common when dependencies are traversed recursively
- Strongly Connected Components (Kosaraju’s) → reuses this exact DFS-finish-order idea as its first pass