Topological Sort
A topological ordering of a directed graph is a linear arrangement of its vertices such that
for every directed edge u → v, vertex u comes before v in the ordering.
Two critical constraints:
- Topological sort is only defined for Directed Acyclic Graphs (DAGs).
- If the graph contains a cycle, no valid ordering exists.
Real-World Intuition
Imagine you have a list of courses and prerequisites. You cannot take "Machine Learning" before you have passed "Linear Algebra". Topological sort gives you a valid study order.
Other examples:
- Dependency resolution (npm install runs packages in dependency order)
- Build systems (compile files whose imports are compiled first)
- Task scheduling (run task B only after task A finishes)
Approach 1 — Kahn's Algorithm (BFS)
Kahn's algorithm uses the concept of in-degree — the number of incoming edges to a vertex.
- Compute in-degree for every vertex.
- Add all vertices with in-degree 0 to a queue (they have no prerequisites).
- Process the queue: for each dequeued vertex, add it to the result and decrease the in-degree of each neighbour. If a neighbour reaches in-degree 0, enqueue it.
- If the result contains all vertices → valid topological order. If not (some vertices remain) → the graph has a cycle.
function topologicalSortBFS(numVertices, edges) {
// Build adjacency list and in-degree array
const graph = Array.from({ length: numVertices }, () => []);
const inDegree = new Array(numVertices).fill(0);
for (const [u, v] of edges) {
graph[u].push(v);
inDegree[v]++;
}
// Queue: all vertices with no prerequisites
const queue = [];
for (let i = 0; i < numVertices; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const order = [];
while (queue.length > 0) {
const node = queue.shift(); // O(n) — use a real queue for production
order.push(node);
for (const neighbour of graph[node]) {
inDegree[neighbour]--;
if (inDegree[neighbour] === 0) queue.push(neighbour);
}
}
// Cycle detection: if order doesn't include all vertices, cycle exists
return order.length === numVertices ? order : [];
}
// 5 courses, prerequisites: 1→0, 2→0, 3→1, 3→2, 4→3
const edges = [[1,0],[2,0],[3,1],[3,2],[4,3]];
console.log(topologicalSortBFS(5, edges)); // [0,1,2,3,4] or similar valid orderApproach 2 — DFS with Finish Timestamps
Run DFS on each unvisited node. A node is added to the front of the result list only after all its descendants have been fully explored (post-order). This guarantees that dependencies come before dependents.
Track three states per node:
- 0 = unvisited
- 1 = currently being visited (in the DFS call stack)
- 2 = fully processed
If you reach a node with state 1, you have found a back edge → cycle detected.
function topologicalSortDFS(numVertices, edges) {
const graph = Array.from({ length: numVertices }, () => []);
for (const [u, v] of edges) graph[u].push(v);
const state = new Array(numVertices).fill(0); // 0=unvisited,1=visiting,2=done
const order = [];
let hasCycle = false;
function dfs(node) {
if (state[node] === 1) { hasCycle = true; return; } // back edge = cycle
if (state[node] === 2) return; // already processed
state[node] = 1; // mark as being visited
for (const neighbour of graph[node]) {
dfs(neighbour);
if (hasCycle) return;
}
state[node] = 2; // done
order.unshift(node); // prepend — post-order gives reverse topological sort
}
for (let i = 0; i < numVertices; i++) {
if (state[i] === 0) dfs(i);
if (hasCycle) return [];
}
return order;
}
const edges2 = [[1,0],[2,0],[3,1],[3,2],[4,3]];
console.log(topologicalSortDFS(5, edges2)); // [4,3,1,2,0] or similarProblem 1 — Course Schedule I (LeetCode 207)
Can you finish all courses given their prerequisites? Equivalent to: "does the graph have a cycle?"
function canFinish(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = new Array(numCourses).fill(0);
for (const [course, pre] of prerequisites) {
graph[pre].push(course);
inDegree[course]++;
}
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
let completed = 0;
while (queue.length > 0) {
const node = queue.shift();
completed++;
for (const next of graph[node]) {
if (--inDegree[next] === 0) queue.push(next);
}
}
return completed === numCourses;
}
console.log(canFinish(2, [[1,0]])); // true (take 0, then 1)
console.log(canFinish(2, [[1,0],[0,1]])); // false (cycle: 0↔1)Problem 2 — Course Schedule II (LeetCode 210)
Return one valid course ordering, or an empty array if impossible. Same as Kahn's algorithm — just return the order array.
function findOrder(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const inDegree = new Array(numCourses).fill(0);
for (const [course, pre] of prerequisites) {
graph[pre].push(course);
inDegree[course]++;
}
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const order = [];
while (queue.length > 0) {
const node = queue.shift();
order.push(node);
for (const next of graph[node]) {
if (--inDegree[next] === 0) queue.push(next);
}
}
return order.length === numCourses ? order : [];
}
console.log(findOrder(4, [[1,0],[2,0],[3,1],[3,2]]));
// [0,1,2,3] or [0,2,1,3]Problem 3 — Alien Dictionary (LeetCode 269)
Given a sorted list of alien-language words, deduce the character ordering of the alien alphabet.
- Compare adjacent words character by character. The first differing character gives an edge:
char1 → char2in the alien alphabet. - Run Kahn's algorithm on these edges. If a cycle is found or not all characters appear, the input is invalid.
function alienOrder(words) {
const graph = new Map();
const inDegree = new Map();
// Initialise graph with all unique characters
for (const word of words) {
for (const ch of word) {
if (!graph.has(ch)) graph.set(ch, new Set());
if (!inDegree.has(ch)) inDegree.set(ch, 0);
}
}
// Build edges from adjacent word comparisons
for (let i = 0; i < words.length - 1; i++) {
const w1 = words[i], w2 = words[i + 1];
const minLen = Math.min(w1.length, w2.length);
// Edge case: "abc" before "ab" is invalid
if (w1.length > w2.length && w1.startsWith(w2)) return '';
for (let j = 0; j < minLen; j++) {
if (w1[j] !== w2[j]) {
if (!graph.get(w1[j]).has(w2[j])) {
graph.get(w1[j]).add(w2[j]);
inDegree.set(w2[j], inDegree.get(w2[j]) + 1);
}
break; // only first differing char matters
}
}
}
// Kahn's algorithm
const queue = [];
for (const [ch, deg] of inDegree) if (deg === 0) queue.push(ch);
let result = '';
while (queue.length > 0) {
const ch = queue.shift();
result += ch;
for (const next of graph.get(ch)) {
inDegree.set(next, inDegree.get(next) - 1);
if (inDegree.get(next) === 0) queue.push(next);
}
}
return result.length === inDegree.size ? result : '';
}
console.log(alienOrder(['wrt','wrf','er','ett','rftt'])); // 'wertf'Complexity Summary
Algorithm | Time | Space | Notes |
|---|---|---|---|
Kahn's (BFS) | O(V + E) | O(V + E) | Easy cycle detection; iterative |
DFS post-order | O(V + E) | O(V + E) | Natural recursion; watch stack depth |
Practice Problems
LeetCode 207 — Course Schedule I (cycle detection)
LeetCode 210 — Course Schedule II (return order)
LeetCode 269 — Alien Dictionary
LeetCode 310 — Minimum Height Trees (iterative leaf trimming = topo sort)
LeetCode 444 — Sequence Reconstruction
LeetCode 2115 — Find All Possible Recipes from Given Supplies