Python Program for Topological Sort (Kahn's Algorithm)
Advanced Graphs Time O(V + E) Space O(V)
Orders courses so that each comes after the courses it needs, with Kahn's algorithm: count each one's prerequisites, then use a queue.
A topological sort puts the nodes of a directed graph in a line so that every edge points forward: if A has to come before B, A is earlier in the line. It answers questions like "in what order can I take these courses?" or "in what order do these build steps have to run?". In the program, Data needs both Python and Maths, so it can only come after both. Kahn's algorithm builds the order by taking, again and again, a node that nothing still waits for. An order exists only when the graph has no cycle: if A needs B and B needs A, neither can go first.
Program
from collections import deque
def topological_sort(graph):
"""The nodes in an order where every edge points forward."""
in_degree = {node: 0 for node in graph}
for node in graph:
for after in graph[node]:
in_degree[after] += 1
queue = deque(node for node in graph if in_degree[node] == 0)
order = []
while queue:
node = queue.popleft()
order.append(node)
for after in graph[node]:
in_degree[after] -= 1
if in_degree[after] == 0: # all it needs is done
queue.append(after)
if len(order) < len(graph):
return None # a cycle: some nodes wait on each other
return order
# Each course, and the courses that build on it
courses = {
"Python": ["Data", "Web"],
"Maths": ["Data"],
"Data": ["ML"],
"Web": ["Project"],
"ML": ["Project"],
"Project": [],
}
print("Course order:", topological_sort(courses))
Open in the editor Download topological_sort.py
Output
Course order: ['Python', 'Maths', 'Web', 'Data', 'ML', 'Project']
How it works
Step through the program line by line: each step below shows up as its line runs.
-
in_degreecounts the edges coming into each node: how many courses it needs first. Data needs Python and Maths, so its count is 2. - The nodes with a count of 0 need nothing, so they can go first: they start the queue, a
dequefrom thecollectionsmodule. -
orderstarts empty. While the queue has nodes, the one at the front goes next in the order. - Now that it's done, every node that needed it needs one fewer. A node whose count reaches 0 has everything it needs, so it joins the queue.
- If some nodes never got to 0, they wait on each other in a cycle and no order exists,
so the function returns
None. Otherwise it returns the order. - The graph is a dictionary: each course is a key, and its list holds the courses that build on it. Python leads to Data and Web; Web and ML both lead to Project.
- The program prints an order to take the courses in.
The counts start at Python 0, Maths 0, Data 2, Web 1, ML 1 and Project 2, so the queue starts with Python and Maths:
| Step | Takes | Counts that drop | Queue after |
|---|---|---|---|
| 1 | Python | Data 2 โ 1, Web 1 โ 0 | Maths, Web |
| 2 | Maths | Data 1 โ 0 | Web, Data |
| 3 | Web | Project 2 โ 1 | Data |
| 4 | Data | ML 1 โ 0 | ML |
| 5 | ML | Project 1 โ 0 | Project |
| 6 | Project | none | empty |
Other orders are right too, such as Maths, Python, Data, Web, ML, Project. Which one Kahn's algorithm gives depends on the order of the dict and its lists.
Time and space complexity
| Time | O(V + E) |
|---|---|
| Extra space | O(V) |
Counting the in-degrees reads every node's list once: O(V + E) for V nodes and E edges. Then each node goes into
the queue once and comes out once, and when it comes out, each of its edges lowers one count: O(V + E) again.
in_degree, the queue and order each hold at most V nodes, so the extra space is O(V).
Other ways to write it
With depth-first search
A depth-first search adds a node to order only after every course that builds on it has been added, so the list
comes out back to front, and reversing it gives an order. Unlike Kahn's algorithm, this version doesn't notice a
cycle.
def topological_sort(graph):
order = []
seen = set()
def visit(node):
seen.add(node)
for after in graph[node]:
if after not in seen:
visit(after)
order.append(node) # after everything that builds on it
for node in graph:
if node not in seen:
visit(node)
return order[::-1]
courses = {"Python": ["Data", "Web"], "Maths": ["Data"],
"Data": ["ML"], "Web": ["Project"], "ML": ["Project"],
"Project": []}
print(topological_sort(courses))With graphlib
Since Python 3.9 the standard library has a topological sort, graphlib.TopologicalSorter. It takes each node with
the nodes it needs first, the other way round from the program's dict, and raises CycleError for a cycle.
from graphlib import TopologicalSorter
needs = {"Data": ["Python", "Maths"], "Web": ["Python"],
"ML": ["Data"], "Project": ["Web", "ML"]}
print(list(TopologicalSorter(needs).static_order()))Try it yourself
- Make Project something Python needs,
"Project": ["Python"]. Now the courses wait on each other in a circle, and the function returnsNone. - Move
"Maths"above"Python"in the dict. The order becomes Maths, Python, Data, Web, ML, Project: different, and still right. - Add a course
"Job": []and put it in Project's list. It comes last. - Group the courses into terms: in each round, take every course in the queue at once, as one term. The terms are
[['Python', 'Maths'], ['Web', 'Data'], ['ML'], ['Project']].