Master topological sorting completely — what it is, why it only works on DAGs, DFS-based algorithm with finish times, Kahn
What Is Topological Sorting?
A topological ordering of a directed graph is an ordering of vertices such that for every directed edge (u, v), vertex u appears before vertex v in the ordering.
Requirements:
- The graph must be a DAG (Directed Acyclic Graph)
- Only directed graphs have topological orderings
Course prerequisites (DAG)
Math101 → Math201 → Math301
↑
CS101 → CS201
Valid topological orders
Math101, CS101, CS201, Math201, Math301
CS101, Math101, CS201, Math201, Math301
CS101, CS201, Math101, Math201, Math301
All orders respect: prerequisite always comes before dependent course.
Algorithm 2 — Kahn's Algorithm (BFS + In-Degrees)
Key insight: In any valid topological ordering, vertices with no predecessors must come first. Process them, remove their edges, repeat.
from collections import deque, defaultdict
def kahn_topological_sort(graph, n_vertices):
"""
Kahn's BFS-based topological sort.
Time: O(V + E), Space: O(V + E)
Returns topological order or detects cycle.
"""
# Count in-degrees for all vertices
in_degree = defaultdict(int)
for u in graph:
for v in graph[u]:
in_degree[v] += 1
# Start with all vertices that have no prerequisites
queue = deque([v for v in graph if in_degree[v] == 0])
result = []
while queue:
u = queue.popleft()
result.append(u)
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v) # All prerequisites met
if len(result) != len(graph):
raise ValueError("Graph has a cycle — topological sort not possible")
return result
graph = {
'Math101': ['Math201'],
'CS101': ['CS201'],
'CS201': ['Math201'],
'Math201': ['Math301'],
'Math301': []
}
print(kahn_topological_sort(graph, 5))
Step-by-step trace:
| In-degrees | Math101:0, CS101:0, CS201:1, Math201:2, Math301:1 |
| Queue | [Math101, CS101] (in-degree 0) |
| Step 1: Dequeue Math101. Remove edge Math101 | Math201. |
| Math201 in-degree | 2-1=1 |
| Queue | [CS101] |
| Step 2: Dequeue CS101. Remove CS101 | CS201. |
| CS201 in-degree: 1-1=0 | Add to queue |
| Queue | [CS201] |
| Step 3: Dequeue CS201. Remove CS201 | Math201. |
| Math201 in-degree: 1-1=0 | Add to queue |
| Queue | [Math201] |
| Step 4: Dequeue Math201. Remove Math201 | Math301. |
| Math301 in-degree: 1-1=0 | Add to queue |
| Queue | [Math301] |
| Step 5 | Dequeue Math301. |
| Queue | [] |
| Result | [Math101, CS101, CS201, Math201, Math301] ✓ |
DFS vs Kahn's Comparison
| Feature | DFS | Kahn's (BFS) |
|---|
| Algorithm type | Recursive DFS | Iterative BFS |
| Cycle detection | GRAY vertex → cycle | len(result) < V → cycle |
| Implementation | Clean with recursion | Explicit in-degree tracking |
| Unique ordering | Depends on DFS order | Depends on queue ordering |
| Parallelism | Harder to parallelize | Easier (process 0-in-degree at same time) |
Applications
Course Schedule (LeetCode 207 & 210)
def can_finish(num_courses, prerequisites):
"""Can you finish all courses given prerequisites?"""
graph = defaultdict(list)
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque(i for i in range(num_courses) if in_degree[i] == 0)
count = 0
while queue:
u = queue.popleft()
count += 1
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
return count == num_courses # True if no cycle
print(can_finish(2, [[1,0]])) # True
print(can_finish(2, [[1,0],[0,1]])) # False (cycle!)
def find_order(num_courses, prerequisites):
"""Return valid course completion order."""
graph = defaultdict(list)
in_degree = [0] * num_courses
for c, p in prerequisites:
graph[p].append(c); in_degree[c] += 1
queue = deque(i for i in range(num_courses) if in_degree[i] == 0)
order = []
while queue:
u = queue.popleft(); order.append(u)
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0: queue.append(v)
return order if len(order) == num_courses else []
Build System (Compile Order)
def build_order(projects, dependencies):
"""
Given list of projects and dependencies (a must be built before b),
find valid build order.
"""
graph = defaultdict(list)
in_degree = {p: 0 for p in projects}
for a, b in dependencies:
graph[a].append(b)
in_degree[b] += 1
result = kahn_topological_sort(dict(graph), len(projects))
return result
projects = ['A','B','C','D','E','F']
deps = [('A','D'),('F','B'),('B','D'),('F','A'),('D','C')]
print(build_order(projects, deps))
Lexicographically Smallest Topological Order
import heapq
def lex_topological_sort(graph):
"""
Kahn's algorithm using min-heap instead of queue
→ always processes smallest vertex among those with 0 in-degree.
"""
in_degree = defaultdict(int)
for u in graph:
for v in graph[u]: in_degree[v] += 1
heap = [v for v in graph if in_degree[v] == 0]
heapq.heapify(heap)
result = []
while heap:
u = heapq.heappop(heap)
result.append(u)
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
heapq.heappush(heap, v)
return result
Summary
Topological sort orders vertices of a DAG such that all edges go "forward":
- DFS approach: Add to result after finishing, reverse the result
- Kahn's (BFS): Process 0-in-degree vertices, decrement neighbors' in-degrees
- Cycle detection: GRAY vertex (DFS) or
len(result) < V (Kahn's) - Time: O(V + E) for both approaches
Use when: Task scheduling, course prerequisites, build systems, dependency resolution, compiler compilation order.
*Next Lesson: Shortest Path Algorithms*