Topological Sort
Order nodes in a directed acyclic graph (DAG) so every edge points forward. Master cycle detection, Kahn's algorithm (BFS), and DFS post-order reversal for dependency ordering, course scheduling, and alien dictionaries.
Learn & Reference
Understanding the Pattern
Topological Sort Pattern
A topological sort produces a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u comes before v in the ordering. If the graph has a cycle, no valid topological ordering exists.
Core Concept
Think of it like course prerequisites: if Course A requires Course B, then B must appear before A in any valid schedule. Topological sort finds such a schedule — or detects that one is impossible (a cycle exists).
Prerequisites: Topological Order:
0 → 1 → 3 [0, 2, 1, 3] or [2, 0, 1, 3]
2 → 1
A DAG can have multiple valid topological orderings. Any ordering where all edges point "forward" is correct.
Key Techniques
1. Kahn's Algorithm (BFS with In-Degrees)
The most intuitive approach. Track the in-degree (number of incoming edges) of each node. Nodes with in-degree 0 have no unsatisfied dependencies and can go first.
1. Build adjacency list and compute in-degree for each node
2. Add all nodes with in-degree 0 to a queue
3. While queue is not empty:
a. Remove node from queue → add to result
b. For each neighbor, decrement its in-degree
c. If neighbor's in-degree becomes 0, add to queue
4. If result has all nodes → valid ordering
If result is shorter → cycle exists
2. DFS Post-Order Reversal
Process nodes in DFS order, and add each node to the result after all its descendants are processed (post-order). Reverse the result to get topological order.
1. For each unvisited node, run DFS
2. In DFS, mark node as "in progress" (on current path)
3. Recurse on all neighbors
4. If we visit an "in progress" node → cycle detected
5. After all neighbors done, mark as "done" and push to stack
6. Pop all from stack → topological order
3. Cycle Detection
Both approaches naturally detect cycles:
- Kahn's: If the result has fewer nodes than the graph, a cycle exists (some nodes never reached in-degree 0)
- DFS: If we encounter a node that's currently "in progress" on the recursion stack, we've found a back edge (cycle)
4. Building the Graph from Constraints
Many problems don't give you an explicit graph — you derive edges from constraints:
- Course prerequisites:
[course, prereq]→ edge from prereq to course - Alien dictionary: Compare adjacent words to find character ordering → edges between characters
Common Mistakes
- Forgetting to handle disconnected components — In Kahn's algorithm, initialize ALL nodes with in-degree 0 into the queue (not just node 0). In DFS, iterate over ALL nodes.
- Wrong edge direction — For prerequisites
[a, b]meaning "b before a," the edge isb → a, nota → b. Getting this backwards produces the reverse ordering. - Not detecting cycles — Always check if the result includes all nodes. Returning a partial ordering when a cycle exists is a common source of wrong answers.
- Confusing "visited" states in DFS — You need THREE states: unvisited, in-progress (on current recursion path), and done. Using only two states (visited/unvisited) can't detect cycles.
- Missing characters in Alien Dictionary — Characters that appear in words but have no ordering constraints still need to be included in the result. Track all unique characters from the input.
When to Use
Template
1# Kahn's Algorithm — Topological Sort (BFS)2from collections import deque34def topo_sort(num_courses, prerequisites):5graph = [[] for _ in range(num_courses)]6in_degree = [0] * num_courses78for course, prereq in prerequisites:9graph[prereq].append(course)10in_degree[course] += 11112queue = deque(i for i in range(num_courses) if in_degree[i] == 0)13order = []1415while queue:16node = queue.popleft()17order.append(node)18for neighbor in graph[node]:19in_degree[neighbor] -= 120if in_degree[neighbor] == 0:21queue.append(neighbor)2223return order if len(order) == num_courses else []
Syntax Reference
1# Build adjacency list + in-degree array2from collections import deque, defaultdict34graph = defaultdict(list) # node -> [neighbors]5in_degree = [0] * num_nodes67for src, dst in edges:8graph[src].append(dst)9in_degree[dst] += 11011# Kahn's: initialize queue with in-degree 0 nodes12queue = deque(i for i in range(num_nodes) if in_degree[i] == 0)1314# Process15while queue:16node = queue.popleft()17for neighbor in graph[node]:18in_degree[neighbor] -= 119if in_degree[neighbor] == 0:20queue.append(neighbor)
Common Recipes
1# Recipe 1: Kahn's Algorithm — Topological Sort (BFS)2from collections import deque, defaultdict34def topo_sort_kahn(num_nodes, edges):5graph = defaultdict(list)6in_degree = [0] * num_nodes7for src, dst in edges:8graph[src].append(dst)9in_degree[dst] += 11011queue = deque(i for i in range(num_nodes) if in_degree[i] == 0)12order = []1314while queue:15node = queue.popleft()16order.append(node)17for neighbor in graph[node]:18in_degree[neighbor] -= 119if in_degree[neighbor] == 0:20queue.append(neighbor)2122return order if len(order) == num_nodes else [] # empty = cycle2324# Recipe 2: DFS Post-Order — Topological Sort25def topo_sort_dfs(num_nodes, edges):26graph = defaultdict(list)27for src, dst in edges:28graph[src].append(dst)2930WHITE, GRAY, BLACK = 0, 1, 231color = [WHITE] * num_nodes32order = []33has_cycle = False3435def dfs(node):36nonlocal has_cycle37if has_cycle:38return39color[node] = GRAY40for neighbor in graph[node]:41if color[neighbor] == GRAY:42has_cycle = True43return44if color[neighbor] == WHITE:45dfs(neighbor)46color[node] = BLACK47order.append(node)4849for i in range(num_nodes):50if color[i] == WHITE:51dfs(i)5253return order[::-1] if not has_cycle else []5455# Recipe 3: Cycle Detection (Kahn's shortcut)56def has_cycle(num_nodes, edges):57graph = defaultdict(list)58in_degree = [0] * num_nodes59for src, dst in edges:60graph[src].append(dst)61in_degree[dst] += 162queue = deque(i for i in range(num_nodes) if in_degree[i] == 0)63count = 064while queue:65node = queue.popleft()66count += 167for neighbor in graph[node]:68in_degree[neighbor] -= 169if in_degree[neighbor] == 0:70queue.append(neighbor)71return count != num_nodes # True if cycle exists
Complexity
| Operation | Time | Space | Notes |
|---|---|---|---|
| Kahn's Algorithm (BFS) | O(V + E) | O(V + E) | Adjacency list + in-degree array + queue |
| DFS Post-Order | O(V + E) | O(V + E) | Adjacency list + color array + recursion stack |
| Cycle Detection | O(V + E) | O(V + E) | Same as topo sort — cycle = incomplete ordering |
| Build graph from edges | O(E) | O(V + E) | One pass to build adjacency list + in-degrees |
Watch Out
- ✗Forgetting to handle disconnected components — In Kahn's algorithm, initialize ALL nodes with in-degree 0 into the queue (not just node 0). In DFS, iterate over ALL nodes.
- ✗Wrong edge direction — For prerequisites `[a, b]` meaning "b before a," the edge is `b → a`, not `a → b`. Getting this backwards produces the reverse ordering.
- ✗Not detecting cycles — Always check if the result includes all nodes. Returning a partial ordering when a cycle exists is a common source of wrong answers.
- ✗Confusing "visited" states in DFS — You need THREE states: unvisited, in-progress (on current recursion path), and done. Using only two states (visited/unvisited) can't detect cycles.
- ✗Missing characters in Alien Dictionary — Characters that appear in words but have no ordering constraints still need to be included in the result. Track all unique characters from the input.