Algorithms · Lesson 13 of 17
Topological Sort
Learn topological sort in Python with Kahn's algorithm and DFS to order tasks, build systems and course prerequisites in a DAG and detect cycles.
- Intermediate
- 16 min read
- 4 objectives
Before this lessonLesson 12: Shortest Paths: Dijkstra and Bellman-Ford
What you will learn
- Explain what a DAG and a topological order are
- Implement Kahn's algorithm with in-degrees
- Implement the DFS post-order approach
- Use the order for scheduling and longest-path problems
Your Progress
0 of 17 lessons 0%
- Lessons0 / 17
- Completed0
- Est. time left~ 5 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
Some tasks must happen before others. You install dependencies before building, build before testing, test before deploying. Course lessons have prerequisites. Spreadsheet cells depend on other cells. A topological sort takes a set of "A must come before B" rules and produces an order that respects all of them.
The rules form a directed graph: an edge A to B means A comes first. An order exists only if there are no cycles (if A needs B and B needs A, nothing can start). A directed graph without cycles is called a DAG (directed acyclic graph). Every DAG has at least one topological order, and often many. Tools you use daily, like pip, npm, Make, Airflow and Python's own graphlib module, run a topological sort under the hood.
The example: a stackcone learning path
prereqs = {
"python": ["pandas", "flask"],
"sql": ["pandas"],
"pandas": ["ml"],
"flask": ["deploy"],
"ml": ["deploy"],
"deploy": [],
}
# an edge u -> v means "take u before v"
for u, vs in prereqs.items():
for v in vs:
print(f"{u} -> {v}")python -> pandas python -> flask sql -> pandas pandas -> ml flask -> deploy ml -> deploy
Kahn's algorithm (BFS with in-degrees)
The in-degree of a node is how many edges point into it: how many prerequisites it still has. A node with in-degree 0 can be done right now. Kahn's algorithm repeatedly takes a ready node, outputs it, and "removes" its outgoing edges by decrementing its neighbours' in-degrees. Any neighbour that drops to 0 becomes ready.
# from earlier in this lesson
prereqs = {
"python": ["pandas", "flask"],
"sql": ["pandas"],
"pandas": ["ml"],
"flask": ["deploy"],
"ml": ["deploy"],
"deploy": [],
}
# an edge u -> v means "take u before v"
# new code
from collections import deque
def kahn(graph):
indegree = {n: 0 for n in graph}
for u in graph:
for v in graph[u]:
indegree[v] += 1
ready = deque(n for n in graph if indegree[n] == 0)
order = []
while ready:
u = ready.popleft()
order.append(u)
for v in graph[u]:
indegree[v] -= 1
if indegree[v] == 0:
ready.append(v)
if len(order) != len(graph):
raise ValueError("cycle detected")
return order
print(kahn(prereqs))['python', 'sql', 'flask', 'pandas', 'ml', 'deploy']
If a cycle exists, the nodes in it never reach in-degree 0, so they never get output. Comparing len(order) with the number of nodes is a free cycle check. Time O(V + E), space O(V).
The DFS approach
The second method uses DFS. When DFS finishes a node (all its descendants are done), everything that must come after it has already been recorded. So if you append each node when it finishes, you get a reverse topological order. Reverse the list at the end.
# from earlier in this lesson
prereqs = {
"python": ["pandas", "flask"],
"sql": ["pandas"],
"pandas": ["ml"],
"flask": ["deploy"],
"ml": ["deploy"],
"deploy": [],
}
# an edge u -> v means "take u before v"
# new code
def topo_dfs(graph):
WHITE, GREY, BLACK = 0, 1, 2
color = {n: WHITE for n in graph}
finished = []
def visit(u):
color[u] = GREY
for v in graph[u]:
if color[v] == GREY:
raise ValueError(f"cycle through {v}")
if color[v] == WHITE:
visit(v)
color[u] = BLACK
finished.append(u) # post-order
for n in graph:
if color[n] == WHITE:
visit(n)
return finished[::-1]
print(topo_dfs(prereqs))
try:
topo_dfs({"a": ["b"], "b": ["a"]})
except ValueError as e:
print("error:", e)['sql', 'python', 'flask', 'pandas', 'ml', 'deploy'] error: cycle through a
Notice the two methods returned different, equally valid orders. Both put python and sql before pandas, and deploy last. Time O(V + E), space O(V) plus recursion depth.
Use the standard library
Since Python 3.9, graphlib.TopologicalSorter does this for you. Note that it takes a mapping from each node to its predecessors (dependencies), the opposite of our adjacency list.
from graphlib import TopologicalSorter, CycleError
depends_on = {"pandas": {"python", "sql"}, "ml": {"pandas"},
"flask": {"python"}, "deploy": {"flask", "ml"}}
order = list(TopologicalSorter(depends_on).static_order())
print(order.index("python") < order.index("pandas") < order.index("ml") < order.index("deploy"))
try:
list(TopologicalSorter({"a": {"b"}, "b": {"a"}}).static_order())
except CycleError:
print("CycleError raised")True CycleError raised
Scheduling: the longest path in a DAG
If every task has a duration and independent tasks can run in parallel, the total project time is the longest path through the DAG (the critical path). Finding longest paths is hard in general graphs, but in a DAG it is easy: process nodes in topological order and relax edges, keeping the maximum.
# from earlier in this lesson
prereqs = {
"python": ["pandas", "flask"],
"sql": ["pandas"],
"pandas": ["ml"],
"flask": ["deploy"],
"ml": ["deploy"],
"deploy": [],
}
# an edge u -> v means "take u before v"
from collections import deque
def kahn(graph):
indegree = {n: 0 for n in graph}
for u in graph:
for v in graph[u]:
indegree[v] += 1
ready = deque(n for n in graph if indegree[n] == 0)
order = []
while ready:
u = ready.popleft()
order.append(u)
for v in graph[u]:
indegree[v] -= 1
if indegree[v] == 0:
ready.append(v)
if len(order) != len(graph):
raise ValueError("cycle detected")
return order
# new code
def project_time(graph, duration):
finish = {}
for u in kahn(graph):
# a task starts when all its prerequisites have finished
start = finish.get(u, 0)
finish[u] = start + duration[u]
for v in graph[u]:
finish[v] = max(finish.get(v, 0), finish[u])
return max(finish.values())
hours = {"python": 20, "sql": 8, "pandas": 10, "flask": 6, "ml": 25, "deploy": 4}
print(project_time(prereqs, hours), "hours")59 hours
Here we reuse finish[v] as "earliest start" before v is processed, then overwrite it with the real finish time. The critical path is python, pandas, ml, deploy: 20 + 10 + 25 + 4 = 59. The same idea gives shortest paths in a DAG in O(V + E), even with negative weights.
Recap
- A topological order lists nodes so every edge points forward; it exists only for DAGs.
- Kahn's algorithm repeatedly outputs nodes with in-degree 0; a short output means a cycle.
- DFS post-order, reversed, is also a valid topological order.
- Both run in O(V + E);
graphlib.TopologicalSorteris the built-in option. - Processing a DAG in topological order solves longest and shortest path problems in linear time.
# Write your solution here
Finished reading? Mark this lesson complete to track your progress.
