Topological Sort
Interviewer cue: “in what order should these tasks/courses/builds run,
given these dependencies” is a topological sort problem. A topological
order of a directed acyclic graph (DAG) is a linear arrangement of its
nodes such that for every directed edge u -> vu -> v, uu comes before vv in the
ordering. It only exists when the graph has no cycles — if uu
transitively depends on itself, there’s no valid order at all.
What you’ll learn
- What a DAG is, and why a topological order only exists when there’s no cycle.
- Kahn’s algorithm: a BFS driven by in-degree counts, which also detects cycles for free.
- DFS-based topological sort: post-order traversal, reversed.
- Why reversed post-order is a valid topological order in the first place.
- Real applications: build/dependency ordering and course scheduling.
A DAG and one of its valid orderings
graph LR
N0["A"] --> N1["B"]
N0 --> N2["C"]
N1 --> N3["D"]
N2 --> N3
N3 --> N4["E"]
A, B, C, D, EA, B, C, D, E is a valid order here (so is A, C, B, D, EA, C, B, D, E — ties between
independent nodes can go either way). What’s not valid is anything placing
DD before BB or CC, since both edges point into DD.
Kahn’s algorithm: BFS driven by in-degree
Track each node’s in-degree (how many edges point into it). Any node
with in-degree 00 has no unfinished prerequisites, so it’s safe to process
right now. Processing a node “removes” its outgoing edges, which may drop a
neighbor’s in-degree to 00 and unlock it next.
from collections import deque, defaultdict
def topological_sort_kahn(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges: # edge u -> v means "u must come before v"
graph[u].append(v)
in_degree[v] += 1
queue = deque(node for node in range(n) if in_degree[node] == 0) # no prerequisites left
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0: # every prerequisite of neighbor is now satisfied
queue.append(neighbor)
if len(order) != n:
return None # fewer nodes processed than exist -> a cycle blocked the rest
return order
edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]
print("topological order:", topological_sort_kahn(5, edges))
cyclic_edges = [(0, 1), (1, 2), (2, 0)]
print("cyclic graph result:", topological_sort_kahn(3, cyclic_edges))from collections import deque, defaultdict
def topological_sort_kahn(n, edges):
graph = defaultdict(list)
in_degree = [0] * n
for u, v in edges: # edge u -> v means "u must come before v"
graph[u].append(v)
in_degree[v] += 1
queue = deque(node for node in range(n) if in_degree[node] == 0) # no prerequisites left
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0: # every prerequisite of neighbor is now satisfied
queue.append(neighbor)
if len(order) != n:
return None # fewer nodes processed than exist -> a cycle blocked the rest
return order
edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]
print("topological order:", topological_sort_kahn(5, edges))
cyclic_edges = [(0, 1), (1, 2), (2, 0)]
print("cyclic graph result:", topological_sort_kahn(3, cyclic_edges))DFS-based topological sort: post-order, reversed
This reuses the exact 3-state (WHITEWHITE / GRAYGRAY / BLACKBLACK) cycle-detection
trick from Depth First Search: run a DFS from every unvisited node, and
append a node to the order only after every one of its neighbors has been
fully explored. Reversing that list at the end gives a valid topological
order.
def topological_sort_dfs(n, edges):
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
WHITE, GRAY, BLACK = 0, 1, 2 # unvisited, currently exploring, fully done
state = [WHITE] * n
order = []
def dfs(node):
state[node] = GRAY
for neighbor in graph[node]:
if state[neighbor] == GRAY:
return False # back edge to a node still on the path -> cycle
if state[neighbor] == WHITE and not dfs(neighbor):
return False
state[node] = BLACK
order.append(node) # append AFTER every descendant is fully processed
return True
for node in range(n):
if state[node] == WHITE:
if not dfs(node):
return None
return order[::-1] # reverse post-order = topological order
edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]
print("topological order (DFS):", topological_sort_dfs(5, edges))
cyclic_edges = [(0, 1), (1, 2), (2, 0)]
print("cyclic graph result (DFS):", topological_sort_dfs(3, cyclic_edges))def topological_sort_dfs(n, edges):
graph = {i: [] for i in range(n)}
for u, v in edges:
graph[u].append(v)
WHITE, GRAY, BLACK = 0, 1, 2 # unvisited, currently exploring, fully done
state = [WHITE] * n
order = []
def dfs(node):
state[node] = GRAY
for neighbor in graph[node]:
if state[neighbor] == GRAY:
return False # back edge to a node still on the path -> cycle
if state[neighbor] == WHITE and not dfs(neighbor):
return False
state[node] = BLACK
order.append(node) # append AFTER every descendant is fully processed
return True
for node in range(n):
if state[node] == WHITE:
if not dfs(node):
return None
return order[::-1] # reverse post-order = topological order
edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)]
print("topological order (DFS):", topological_sort_dfs(5, edges))
cyclic_edges = [(0, 1), (1, 2), (2, 0)]
print("cyclic graph result (DFS):", topological_sort_dfs(3, cyclic_edges))Why reversed post-order works
A node is only appended to orderorder once every node reachable from it has
already been appended — so in the raw orderorder list, every node’s
descendants appear before it. Reversing the list flips that, so every
node’s descendants appear after it, and every dependency appears before
the thing that depends on it. That’s exactly the topological property.
graph LR
N0["post-order: D, B, C, A, E"] --> N1["reversed: E, A, C, B, D"]
Applications: build systems and course scheduling
- Build/dependency ordering: a package manager or build tool models
“package A requires package B” as an edge
B -> AB -> A, then topologically sorts to decide install/compile order. A cycle here means two packages depend on each other — an unresolvable dependency loop. - Course scheduling: “course B requires course A” becomes edge
A -> BA -> B. Course Schedule asks if a valid order exists (equivalent to “no cycle”); Course Schedule II asks you to actually produce that order. - Task scheduling with prerequisites in general — spreadsheet cell recalculation order, compiler instruction scheduling, and Makefile target ordering all reduce to the same problem.
Complexity
| Algorithm | Time | Space | Cycle detection |
|---|---|---|---|
| Kahn’s (BFS) | len(order) != nlen(order) != n — some nodes never reached in-degree 0 | ||
| DFS-based | (recursion stack) | A GRAYGRAY neighbor — a back edge to a node still on the current path |
Practice — real LeetCode problems
Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 207 — Course Schedule · Medium
Problem. Given numCoursesnumCourses and prerequisite pairs [course, prereq][course, prereq], return
TrueTrue if all courses can be finished.
Constraints. 1 <= numCourses <= 20001 <= numCourses <= 2000, 0 <= len(prerequisites) <= 50000 <= len(prerequisites) <= 5000, no
duplicate pairs.
Examples. 2, [[1,0]]2, [[1,0]] gives TrueTrue · 2, [[1,0],[0,1]]2, [[1,0],[0,1]] gives FalseFalse
Editorial
“Can all courses be finished?” is “is the prerequisite graph acyclic?“. A topological order exists precisely for a DAG.
Time . Space .
Kahn’s algorithm is the BFS formulation: repeatedly take a node with no remaining prerequisites. The elegance is that cycle detection is free — nodes inside a cycle never reach in-degree 0, so they are never queued, and the processed count falls short.
The edge direction is the detail to get right. [course, pre][course, pre] means prepre must come
first, so the edge runs pre -> coursepre -> course and coursecourse’s in-degree increases. Reversing
this produces a graph that is still acyclic when the original is, so simple tests pass
and ordering problems (LC 210) then fail — worth being careful.
(3, [[1,0],[2,1],[0,2]])(3, [[1,0],[2,1],[0,2]]) is a three-node cycle, giving FalseFalse.
The DFS alternative uses three-colour marking, as in LC 802. Kahn’s is usually easier to get right and gives the order for free.
Follow-ups: “Return the actual order (LC 210)?” — next problem. “Detect the cycle itself?” — DFS with three colours records the path. “Iterative DFS?” — doable but fiddlier than Kahn’s. “Why is a topological order equivalent to acyclicity?” — a cycle has no valid first element; conversely every DAG has a node of in-degree 0.
LC 210 — Course Schedule II · Medium
Problem. Return any valid order in which to take all courses, or an empty array if impossible.
Constraints. 1 <= numCourses <= 20001 <= numCourses <= 2000, 0 <= len(prerequisites) <= 50000 <= len(prerequisites) <= 5000.
Examples. 4, [[1,0],[2,0],[3,1],[3,2]]4, [[1,0],[2,0],[3,1],[3,2]] gives [0,1,2,3][0,1,2,3] or
[0,2,1,3][0,2,1,3] · 2, [[1,0],[0,1]]2, [[1,0],[0,1]] gives [][]
Editorial
The same Kahn’s algorithm, with one line added: append each node as it is dequeued. That sequence is a topological order, because a node is only dequeued once all its prerequisites have been.
Time . Space .
The edge direction is where this problem punishes what LC 207 forgives. Reversing
the edges still detects cycles correctly, so LC 207 would pass — but the emitted order
comes out reversed, and every prerequisite check fails. [course, pre][course, pre] means the edge
runs pre -> coursepre -> course.
Using a dequedeque and popleftpopleft gives the BFS order; a plain list with pop()pop() also
produces a valid topological order, just a different one. Since any valid order is
accepted, either works — which is why the test validates the property rather than
comparing to a fixed answer.
The DFS alternative produces the order by reversed post-order: append each node after exploring all its successors, then reverse. Worth knowing, and it naturally detects cycles with three-colour marking.
Follow-ups: “Lexicographically smallest order?” — use a heap instead of a queue. “All valid orders?” — exponentially many; backtracking. “How many valid orders?” — counting linear extensions is #P-complete in general. “Parallel course scheduling (LC 1136)?” — count Kahn’s rounds rather than nodes; each round is one semester.
LC 310 — Minimum Height Trees · Medium
Problem. Given a tree with nn nodes, return all root labels that produce a
minimum-height rooted tree.
Constraints. 1 <= n <= 2 * 10^41 <= n <= 2 * 10^4, edgesedges forms a tree.
Examples. n = 4, edges = [[1,0],[1,2],[1,3]]n = 4, edges = [[1,0],[1,2],[1,3]] gives [1][1] ·
n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]] gives [3,4][3,4]
Editorial
The minimum-height roots are the tree’s centroids, and the key fact is that a tree has exactly one or two of them — never more. So peel leaves inward until at most two nodes survive.
Time — each node is removed once. Space .
Why peeling works: the deepest nodes in any rooted tree are leaves, so removing all leaves reduces every node’s height by exactly one and preserves which node is best. The process is Kahn’s algorithm with “degree 1” standing in for “in-degree 0”.
Why two and not one: on a path with an even number of nodes, the two middle nodes
are equally good roots. (2, [[0,1]])(2, [[0,1]]) is the minimal case, giving [0, 1][0, 1]. Stopping
the loop at > 1> 1 would peel one of them away and lose an answer.
n == 1n == 1 needs its own line: a single node has degree 0, so it is never in the initial
leavesleaves list, and the loop would return an empty result.
Using setset for the adjacency makes discarddiscard and poppop clean; with lists you would
need removeremove, which is .
Follow-ups: “Why at most two centroids?” — the standard proof: if three existed, two would be adjacent to a third and one could be improved. Be ready to sketch it. “Compute the actual minimum height?” — count the peeling rounds. “Diameter of the tree?” — two BFS passes, and the centroids sit at the diameter’s midpoint. “Not a tree?” — the argument collapses; centroids are a tree-specific notion.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 207 | Course Schedule | Medium | Does a valid order exist at all (equivalent to “is this graph acyclic”)? |
| 210 | Course Schedule II | Medium | Return the actual order, using either algorithm above |
| 269 | Alien Dictionary | Hard · Premium | Build the edges yourself first: compare adjacent words letter by letter to infer letter -> letterletter -> letter ordering constraints, then topologically sort the alphabet |
| 310 | Minimum Height Trees | Medium | Not a direct topo sort, but solved by repeatedly peeling off leaves (nodes with in/out-degree 1) layer by layer, the same “process degree-1 nodes, update neighbors” shape as Kahn’s |
| 1136 | Parallel Courses | Medium · Premium | Kahn’s algorithm directly, counting how many rounds (BFS layers) it takes to clear the whole queue |
Recap
- A topological order only exists for a DAG — a directed graph with no cycles.
- Kahn’s algorithm: BFS seeded with every in-degree-0 node; a cycle
reveals itself as
len(order) != nlen(order) != n. - DFS-based: append each node to
orderorderafter all its descendants are done, then reverse; a cycle reveals itself as aGRAYGRAYback edge. - Both run in time and both double as cycle detectors — pick whichever fits the surrounding code (BFS-style queue vs. recursive DFS).
Next up in Phase 7: shortest-path algorithms (Dijkstra, Bellman-Ford) that build directly on the weighted-graph representations, DSU, and traversal patterns covered so far.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
