Topological Sort
What Is Topological Sort?
A topological sort of a directed graph is a linear ordering of its vertices such that for every directed edge u → v, vertex u appears before vertex v in the ordering.
DAG:
5 ──→ 0 ←── 4
│ │
└──→ 2 ──→ 3 ─┘
│
└──→ 1
Valid topological orderings (multiple exist):
[5, 4, 2, 3, 1, 0] ← one valid ordering
[4, 5, 2, 3, 1, 0] ← another valid ordering
[5, 4, 0, 2, 3, 1] ← another valid ordering
RULE: for every edge u→v, u must appear BEFORE v.
Verify [5, 4, 2, 3, 1, 0]:
5→0: 5 is at index 0, 0 is at index 5. 0 < 5 ✓
5→2: 5 is at index 0, 2 is at index 2. 0 < 2 ✓
4→0: 4 is at index 1, 0 is at index 5. 1 < 5 ✓
4→3: 4 is at index 1, 3 is at index 3. 1 < 3 ✓
2→3: 2 is at index 2, 3 is at index 3. 2 < 3 ✓
3→1: 3 is at index 3, 1 is at index 4. 3 < 4 ✓
All edges satisfied ✓
Real-World Applications
COURSE SCHEDULING:
Courses as vertices. Edge A→B means "take A before B".
Topological sort gives a valid study order.
Math101 ──→ Math201 ──→ Math301
Math101 ──→ CS101 ──→ CS201
CS101 ──→ CS301
Valid order: Math101, CS101, Math201, CS101, CS201, CS301, Math301
BUILD SYSTEMS (Make, Gradle, CMake):
Files as vertices. Edge A→B means "compile A before B".
Topological sort gives valid compilation order.
TASK SCHEDULING:
Tasks as vertices. Edge A→B means "A must finish before B starts".
Any topological order is a valid execution plan.
PACKAGE MANAGERS (npm, pip, apt):
Packages as vertices. Edge A→B means "install A before B".
Topological sort gives valid installation order.
Approach 1: DFS Post-Order Topological Sort
Use DFS. When a vertex finishes (all its descendants explored), push it onto a stack. Pop the stack at the end for topological order.
WHY POST-ORDER WORKS:
For edge u→v: DFS explores v (and all its descendants) BEFORE
finishing u. So v is pushed to stack BEFORE u.
When we pop the stack, u comes out BEFORE v.
u → v means u before v ✓
ALGORITHM:
visited = [false] * V
stack = []
for each unvisited vertex (outer loop):
dfs(vertex, visited, stack)
dfs(u, visited, stack):
visited[u] = true
for each neighbour v of u:
if not visited[v]: dfs(v)
stack.push(u) ← push AFTER all descendants finished (post-order)
topological order = stack popped top-to-bottom (or reversed)
1import java.util.*;
2
3public class TopologicalSortDFS {
4
5 // DFS helper — pushes to stack after all descendants are done
6 public static void dfs(int node,
7 ArrayList<ArrayList<Integer>> graph,
8 boolean[] visited,
9 Stack<Integer> stack) {
10 visited[node] = true;
11
12 for (int nbr : graph.get(node)) {
13 if (!visited[nbr]) {
14 dfs(nbr, graph, visited, stack);
15 }
16 }
17
18 stack.push(node); // Push AFTER all descendants (post-order)
19 }
20
21 public static List<Integer> topoSort(int V,
22 ArrayList<ArrayList<Integer>> graph) {
23 boolean[] visited = new boolean[V];
24 Stack<Integer> stack = new Stack<>();
25
26 // Outer loop handles disconnected components
27 for (int i = 0; i < V; i++) {
28 if (!visited[i]) {
29 dfs(i, graph, visited, stack);
30 }
31 }
32
33 // Pop stack → topological order
34 List<Integer> result = new ArrayList<>();
35 while (!stack.isEmpty()) {
36 result.add(stack.pop());
37 }
38 return result;
39 }
40
41 public static void main(String[] args) {
42 Scanner sc = new Scanner(System.in);
43 int V = sc.nextInt();
44 int E = sc.nextInt();
45
46 ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
47 for (int i = 0; i < V; i++) graph.add(new ArrayList<>());
48
49 for (int i = 0; i < E; i++) {
50 int u = sc.nextInt();
51 int v = sc.nextInt();
52 graph.get(u).add(v); // Directed edge u → v
53 }
54
55 List<Integer> order = topoSort(V, graph);
56 System.out.println("Topological Order (DFS): " + order);
57 }
58}
59
60/*
61Sample Input:
626 6
635 0
645 2
654 0
664 1
672 3
683 1
69
70Sample Output:
71Topological Order (DFS): [5, 4, 2, 3, 1, 0]
72 (or another valid ordering)
73*/Input: 6 6 5 0 5 2 4 0 4 1 2 3 3 1 Output: Topological Order (DFS): [5, 4, 2, 3, 1, 0]
Dry Run: DFS Topological Sort
DAG: 5→0, 5→2, 4→0, 4→1, 2→3, 3→1
Adjacency list:
0: []
1: []
2: [3]
3: [1]
4: [0, 1]
5: [0, 2]
visited=[F,F,F,F,F,F], stack=[]
Outer loop i=0: not visited
dfs(0): visited[0]=T, no neighbours → stack.push(0) → stack=[0]
Outer loop i=1: not visited
dfs(1): visited[1]=T, no neighbours → stack.push(1) → stack=[0,1]
Outer loop i=2: not visited
dfs(2): visited[2]=T
nbr=3: not visited
dfs(3): visited[3]=T
nbr=1: visited → skip
stack.push(3) → stack=[0,1,3]
stack.push(2) → stack=[0,1,3,2]
Outer loop i=3: already visited → skip
Outer loop i=4: not visited
dfs(4): visited[4]=T
nbr=0: visited → skip
nbr=1: visited → skip
stack.push(4) → stack=[0,1,3,2,4]
Outer loop i=5: not visited
dfs(5): visited[5]=T
nbr=0: visited → skip
nbr=2: visited → skip
stack.push(5) → stack=[0,1,3,2,4,5]
Pop stack → [5, 4, 2, 3, 1, 0] ✓
Verify: every edge u→v has u before v:
5→0: 5 at 0, 0 at 5 ✓
5→2: 5 at 0, 2 at 2 ✓
4→0: 4 at 1, 0 at 5 ✓
4→1: 4 at 1, 1 at 4 ✓
2→3: 2 at 2, 3 at 3 ✓
3→1: 3 at 3, 1 at 4 ✓
Approach 2: Kahn's Algorithm (BFS / In-Degree Method)
Compute in-degrees. Start BFS with all zero-in-degree vertices. Process and remove vertices one by one, revealing new zero-in-degree vertices.
KAHN'S ALGORITHM:
1. Compute in-degree of every vertex.
2. Enqueue all vertices with in-degree = 0.
3. While queue not empty:
u = dequeue
add u to result
for each v in adj[u]:
in-degree[v]--
if in-degree[v] == 0: enqueue v
4. If result.size == V: valid topological order.
If result.size < V: cycle exists (not a DAG).
INTUITION — PEELING LAYERS:
Start with vertices that have no prerequisites (in-degree 0).
After "doing" them, mark their outgoing edges as complete.
New vertices become ready (in-degree drops to 0).
Repeat — like peeling layers off an onion.
DAG: 5→0, 5→2, 4→0, 4→1, 2→3, 3→1
In-degrees:
0:2 1:2 2:1 3:1 4:0 5:0
Queue initially: [4, 5] (in-degree 0)
Process 4: result=[4]
4→0: in-degree[0] = 2-1 = 1
4→1: in-degree[1] = 2-1 = 1
Queue: [5]
Process 5: result=[4,5]
5→0: in-degree[0] = 1-1 = 0 → enqueue 0
5→2: in-degree[2] = 1-1 = 0 → enqueue 2
Queue: [0, 2]
Process 0: result=[4,5,0]
0 has no outgoing edges.
Queue: [2]
Process 2: result=[4,5,0,2]
2→3: in-degree[3] = 1-1 = 0 → enqueue 3
Queue: [3]
Process 3: result=[4,5,0,2,3]
3→1: in-degree[1] = 1-1 = 0 → enqueue 1
Queue: [1]
Process 1: result=[4,5,0,2,3,1]
1 has no outgoing edges.
Queue: []
result.size = 6 = V → No cycle! ✓
Topological order: [4, 5, 0, 2, 3, 1]
1import java.util.*;
2
3public class TopologicalSortKahns {
4
5 public static List<Integer> kahnSort(int V,
6 ArrayList<ArrayList<Integer>> graph) {
7 // Step 1: Compute in-degrees
8 int[] inDegree = new int[V];
9 for (int u = 0; u < V; u++) {
10 for (int v : graph.get(u)) {
11 inDegree[v]++;
12 }
13 }
14
15 // Step 2: Enqueue all vertices with in-degree 0
16 Queue<Integer> queue = new LinkedList<>();
17 for (int i = 0; i < V; i++) {
18 if (inDegree[i] == 0) queue.offer(i);
19 }
20
21 // Step 3: BFS — peel vertices one by one
22 List<Integer> result = new ArrayList<>();
23
24 while (!queue.isEmpty()) {
25 int u = queue.poll();
26 result.add(u);
27
28 for (int v : graph.get(u)) {
29 inDegree[v]--;
30 if (inDegree[v] == 0) queue.offer(v);
31 }
32 }
33
34 // Step 4: Cycle check
35 if (result.size() != V) {
36 System.out.println("Cycle detected — topological sort impossible!");
37 return new ArrayList<>();
38 }
39
40 return result;
41 }
42
43 public static void main(String[] args) {
44 Scanner sc = new Scanner(System.in);
45 int V = sc.nextInt();
46 int E = sc.nextInt();
47
48 ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
49 for (int i = 0; i < V; i++) graph.add(new ArrayList<>());
50
51 for (int i = 0; i < E; i++) {
52 int u = sc.nextInt();
53 int v = sc.nextInt();
54 graph.get(u).add(v);
55 }
56
57 List<Integer> order = kahnSort(V, graph);
58 if (!order.isEmpty()) {
59 System.out.println("Topological Order (Kahn's): " + order);
60 }
61 }
62}
63
64/*
65Sample Input (DAG):
666 6
675 0
685 2
694 0
704 1
712 3
723 1
73
74Sample Output:
75Topological Order (Kahn's): [4, 5, 0, 2, 3, 1]
76
77Sample Input (cyclic):
783 3
790 1
801 2
812 0
82
83Sample Output:
84Cycle detected — topological sort impossible!
85*/DAG Input: 6 6 5 0 5 2 4 0 4 1 2 3 3 1 Output: Topological Order (Kahn's): [4, 5, 0, 2, 3, 1] Cyclic Input: 3 3 0 1 1 2 2 0 Output: Cycle detected — topological sort impossible!
Application: Course Schedule
Classic LeetCode problem — can you finish all courses given prerequisites?
Problem:
n courses labeled 0 to n-1.
prerequisites[i] = [a, b] means you must take b before a.
Can you finish all courses?
Model as directed graph:
Edge b → a (b must come before a)
Example:
n=4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Edges: 0→1, 0→2, 1→3, 2→3
In-degrees: 0:0, 1:1, 2:1, 3:2
Kahn's:
Queue=[0]
Process 0: result=[0], reduce 1→0, 2→0 → queue=[1,2]
Process 1: result=[0,1], reduce 3→1 → 3's in-degree=1
Process 2: result=[0,1,2], reduce 3→0 → 3's in-degree=0 → queue=[3]
Process 3: result=[0,1,2,3]
result.size(4) == n(4) → CAN finish! ✓
Valid order: [0, 1, 2, 3] or [0, 2, 1, 3]
1import java.util.*;
2
3public class CourseSchedule {
4
5 // Returns valid course order, or empty list if impossible
6 public static int[] findOrder(int numCourses, int[][] prerequisites) {
7 List<List<Integer>> graph = new ArrayList<>();
8 int[] inDegree = new int[numCourses];
9
10 for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>());
11
12 // Build graph: prerequisite → course
13 for (int[] pre : prerequisites) {
14 int course = pre[0], prereq = pre[1];
15 graph.get(prereq).add(course); // prereq → course
16 inDegree[course]++;
17 }
18
19 // Kahn's topological sort
20 Queue<Integer> queue = new LinkedList<>();
21 for (int i = 0; i < numCourses; i++) {
22 if (inDegree[i] == 0) queue.offer(i);
23 }
24
25 int[] order = new int[numCourses];
26 int idx = 0;
27
28 while (!queue.isEmpty()) {
29 int u = queue.poll();
30 order[idx++] = u;
31
32 for (int v : graph.get(u)) {
33 if (--inDegree[v] == 0) queue.offer(v);
34 }
35 }
36
37 // If all courses processed → valid order; else cycle → impossible
38 return idx == numCourses ? order : new int[]{};
39 }
40
41 public static void main(String[] args) {
42 // Can finish all 4 courses?
43 int[][] prereqs1 = {{1,0},{2,0},{3,1},{3,2}};
44 int[] result1 = findOrder(4, prereqs1);
45 System.out.println("Order: " + Arrays.toString(result1));
46 // [0, 1, 2, 3] or [0, 2, 1, 3]
47
48 // Impossible: 0↔1 form a cycle
49 int[][] prereqs2 = {{1,0},{0,1}};
50 int[] result2 = findOrder(2, prereqs2);
51 System.out.println("Order: " + Arrays.toString(result2)); // []
52 }
53}Output:
Order: [0, 1, 2, 3]
Order: [] (impossible — cycle)
DFS vs Kahn's — Side by Side
SAME DAG: 5→0, 5→2, 4→0, 4→1, 2→3, 3→1
DFS (post-order + reverse) Kahn's (BFS in-degree)
─────────────────────────────────────────────────────────────
Result [5, 4, 2, 3, 1, 0] [4, 5, 0, 2, 3, 1]
Both are valid topological orderings ✓
COMPARISON:
Feature DFS Approach Kahn's Approach
─────────────────────────────────────────────────────────────
Traversal style Recursive DFS Iterative BFS
Data structure Stack + call stack Queue
Cycle detection Separate check Built-in (count < V)
Result Push at DFS exit Append at dequeue
Order Reverse of push Direct append
Disconnected graphs Outer loop needed Outer loop NOT needed
Recursion limit risk Yes (deep graphs) No (iterative)
Memory O(V) stack O(V) queue
Preferred for Path-based logic Scheduling, cycle check
Time complexity O(V + E) O(V + E)
WHEN TO CHOOSE DFS:
✓ Problem involves DFS path tracking simultaneously
✓ Need to identify back edges / strongly connected components
✓ Recursive solution is cleaner for the problem structure
WHEN TO CHOOSE KAHN'S:
✓ Cycle detection is also needed (count < V tells you)
✓ Avoiding recursion stack overflow (large graphs)
✓ Course scheduling, build systems — natural "prerequisite" model
✓ Want to process vertices at same level simultaneously
Complexity Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| DFS post-order | O(V + E) | O(V) | Call stack + result stack |
| Kahn's BFS | O(V + E) | O(V) | In-degree array + queue |
Both visit each vertex once and each edge once — O(V + E) total.
Space: O(V) for visited array, stack/queue, and in-degree array.
Common Mistakes
DFS: pushing vertex at entry (pre-order) instead of exit (post-order). If you push u when you first visit it, u goes to the stack before any of its dependencies. After reversal, u appears after its dependencies — backwards! Always push AFTER all recursive calls complete (post-order = DFS exit).
Kahn's: not building the graph correctly for course schedule problems. If prerequisites[i] = [a, b] means "take b before a", the edge must be b → a (prerequisite → dependent). Building a → b reverses the dependency and gives wrong results. The in-degree of the dependent (a) must be incremented, not the prerequisite (b).
Forgetting the outer loop in DFS for disconnected DAGs. DFS from one vertex only covers its reachable component. If the DAG has isolated vertices or separate components, the outer loop for i in range(V): if not visited[i]: dfs(i) is essential. Kahn's doesn't need this — its initial scan over all vertices handles all components.
Kahn's: not checking result.size() == V for cycle detection. If you skip this check and just return result, a cyclic graph silently returns a partial ordering of non-cycle vertices. Always verify the count equals V — if not, the missing vertices form a cycle and no valid topological order exists.
Multiple valid orderings: assuming uniqueness. Topological sort is not unique when multiple vertices have in-degree 0 simultaneously. If the problem requires a specific valid ordering (e.g., lexicographically smallest), use a min-heap (priority queue) instead of a regular queue in Kahn's.
Interview Questions
Q: Why does topological sort only work on DAGs?
Topological sort requires placing every edge u→v such that u comes before v. In a cycle A→B→C→A: A must come before B, B before C, and C before A — which requires A to come both before and after B simultaneously. This contradiction makes any linear ordering impossible. No valid topological order exists for cyclic directed graphs.
Q: How does Kahn's algorithm detect cycles as a side effect?
Kahn's processes vertices in topological order by always picking a vertex with in-degree 0. In a directed cycle, every cycle vertex always has at least one incoming edge from another cycle vertex — its in-degree never reaches 0 while cycle members remain. These vertices are never enqueued, never processed, never counted. After Kahn's completes, if processed count < V, those unprocessed vertices form a cycle.
Q: When does Kahn's produce a unique topological ordering?
Kahn's produces a unique ordering if and only if, at every step, exactly one vertex has in-degree 0 in the remaining unprocessed subgraph. This happens when the DAG is a Hamiltonian path — a single directed chain where each vertex has exactly one predecessor and one successor. If at any point multiple vertices have in-degree 0, they can be ordered in any way — the topological sort is non-unique.
Summary
Topological sort linearly orders vertices of a DAG such that every edge u→v has u before v. Only works on Directed Acyclic Graphs.
Two approaches — both O(V + E):
DFS (post-order):
- ›Run DFS; push each vertex to stack AFTER all its descendants finish
- ›Pop the stack — this gives topological order
- ›Outer loop ensures all components are covered
Kahn's (BFS / in-degree):
- ›Compute in-degree for all vertices
- ›Enqueue all zero in-degree vertices
- ›BFS: process, decrement neighbours' in-degrees, enqueue newly zeroed vertices
- ›Check: if processed count < V → cycle exists
| DFS | Kahn's | |
|---|---|---|
| Style | Recursive | Iterative |
| Cycle detection | Separate step needed | Built-in (count < V) |
| Risk | Stack overflow for deep graphs | None |
| Best for | Path-tracking problems | Scheduling, cycle check, large graphs |
Classic problems: Course Schedule I/II, Alien Dictionary, Build Order, Task Dependencies.
In the next topic you will explore Shortest Paths — Dijkstra's algorithm for positive-weight graphs and Bellman-Ford for negative weights.
Topological sort is ONLY possible on which type of graph?