DSA course menu
Topological Sort Explained: Kahn's Algorithm and DFS
Learn topological sorting of a directed acyclic graph: ordering tasks with dependencies using Kahn's algorithm (in-degrees and a queue) and DFS postorder, detecting impossible orders, and the course schedule problem.
On this page
A topological order of a directed graph lists the vertices so that every edge u → v has u before v. If edges mean "must happen before", a topological order is a valid schedule.
- Courses: take "Python" before "DSA" before "Machine Learning".
- Build tools compile a file's dependencies before the file.
- Spreadsheets recalculate a cell after the cells it refers to.
A topological order exists only for a DAG (directed acyclic graph). With a cycle (A needs B, B needs A) no order works.
Kahn's algorithm (BFS style)#
- Count each vertex's in-degree: how many edges point into it (how many prerequisites it has).
- Put every vertex with in-degree 0 in a queue: they can be done now.
- Repeatedly take a vertex from the queue, add it to the order, and "remove" its outgoing edges by decreasing its neighbours' in-degrees. Any neighbour that reaches 0 joins the queue.
- If the order contains every vertex, it is valid. Otherwise there is a cycle.
from collections import deque
def topo_sort(graph):
in_degree = {v: 0 for v in graph}
for u in graph:
for v in graph[u]:
in_degree[v] += 1
queue = deque(sorted(v for v in graph if in_degree[v] == 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) == len(graph) else None
courses = {
"python": ["dsa", "data"],
"math": ["ml"],
"dsa": ["ml"],
"data": ["ml"],
"ml": [],
}
print(topo_sort(courses))
cyclic = {"a": ["b"], "b": ["c"], "c": ["a"]}
print(topo_sort(cyclic))['math', 'python', 'dsa', 'data', 'ml']
None"python" and "math" have no prerequisites, so they come first; "ml" has three and comes last. On the cyclic graph no vertex ever reaches in-degree 0, so the result is None. The cost is O(V + E).
A graph can have many valid topological orders; sorting the starting vertices just makes the output predictable.
DFS postorder#
The DFS version adds a vertex to a list after all its descendants are finished (postorder), then reverses the list. A vertex finishes only after everything that depends on it, so the reversed finishing order puts prerequisites first:
def topo_sort_dfs(graph):
visited, finished = set(), []
def visit(u):
visited.add(u)
for v in graph[u]:
if v not in visited:
visit(v)
finished.append(u) # all of u's dependents are done
for u in sorted(graph):
if u not in visited:
visit(u)
return finished[::-1]
courses = {
"python": ["dsa", "data"],
"math": ["ml"],
"dsa": ["ml"],
"data": ["ml"],
"ml": [],
}
print(topo_sort_dfs(courses))['python', 'math', 'dsa', 'data', 'ml']This order is different from Kahn's, but it is equally valid: every course still comes after its prerequisites. (This simple version assumes there is no cycle; combine it with the three-colour check from DFS to detect one.)
Checking an order#
It is easy to verify any proposed order: every edge must point forward.
def is_valid_order(graph, order):
position = {v: i for i, v in enumerate(order)}
return all(position[u] < position[v] for u in graph for v in graph[u])
courses = {"python": ["dsa", "data"], "math": ["ml"], "dsa": ["ml"], "data": ["ml"], "ml": []}
print(is_valid_order(courses, ["math", "python", "data", "dsa", "ml"]))
print(is_valid_order(courses, ["python", "ml", "dsa", "data", "math"]))True
FalseThe second order puts "ml" before "dsa", "data" and "math", which it depends on.
Common mistakes#
Practice#
Practice exercise
You have n courses numbered 0 to n−1 and a list of pairs [course, prerequisite]. Write can_finish(n, pairs) that returns True if all courses can be completed.
Show hint
Build edges prerequisite → course, count in-degrees, and run Kahn's algorithm. All courses can be finished exactly when every course gets processed.
Show solution
from collections import deque
def can_finish(n, pairs):
graph = {i: [] for i in range(n)}
in_degree = [0] * n
for course, pre in pairs:
graph[pre].append(course)
in_degree[course] += 1
queue = deque(i for i in range(n) if in_degree[i] == 0)
done = 0
while queue:
u = queue.popleft()
done += 1
for v in graph[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
queue.append(v)
return done == n
print(can_finish(2, [[1, 0]]))
print(can_finish(2, [[1, 0], [0, 1]]))
print(can_finish(4, [[1, 0], [2, 1], [3, 2]]))True
False
TrueKey takeaway#
Next: shortest paths when edges have weights, with Dijkstra's Shortest Path.
Finished this lesson?