Skip to content

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

diagram A DAG and a valid topological order: every arrow points from earlier to later mermaid

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.

kahns_topo_sort.py
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))
kahns_topo_sort.py
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))
sketch Kahn's algorithm processing one zero-in-degree node at a time p5.js
Each tick pops the next node whose prerequisites are all satisfied, then decrements the in-degree of its neighbors -- watch a node turn gold the instant its last prerequisite finishes.

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.

dfs_topo_sort.py
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))
dfs_topo_sort.py
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.

diagram DFS post-order for the DAG above, then reversed mermaid

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

AlgorithmTimeSpaceCycle detection
Kahn’s (BFS)O(V+E)O(V + E)O(V+E)O(V + E)len(order) != nlen(order) != n — some nodes never reached in-degree 0
DFS-basedO(V+E)O(V + E)O(V)O(V) (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 O(V+E)O(V + E). Space O(V+E)O(V + E).

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 O(V+E)O(V + E). Space O(V+E)O(V + E).

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 O(n)O(n) — each node is removed once. Space O(n)O(n).

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 O(deg)O(\deg).

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

#ProblemDifficultyThe twist
207Course ScheduleMediumDoes a valid order exist at all (equivalent to “is this graph acyclic”)?
210Course Schedule IIMediumReturn the actual order, using either algorithm above
269Alien DictionaryHard · PremiumBuild the edges yourself first: compare adjacent words letter by letter to infer letter -> letterletter -> letter ordering constraints, then topologically sort the alphabet
310Minimum Height TreesMediumNot 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
1136Parallel CoursesMedium · PremiumKahn’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 orderorder after all its descendants are done, then reverse; a cycle reveals itself as a GRAYGRAY back edge.
  • Both run in O(V+E)O(V + E) 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 coffee

Was this page helpful?

Let us know how we did