Same graph as Course Schedule β courses are nodes, [a, b] means edge a -> b (βa depends on bβ) β but now the goal is to actually produce a valid order, not just detect whether one exists. That is precisely a topological sort: an ordering of nodes such that every edge a -> b places b before a. If the graph has a cycle, no such ordering exists and the answer is [].
Running example: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] (course 1 needs 0; course 2 needs 0; course 3 needs both 1 and 2).
| Course | Depends on |
|---|---|
| 0 | (none) |
| 1 | 0 |
| 2 | 0 |
| 3 | 1, 2 |
DFS Post-Order Topological Sort
Time O(V + E)Space O(V + E)Run the same cycle-detecting DFS as Course Schedule, but this time append each course to an order list right after all of its dependencies have finished processing. Because a course is only appended once every course it depends on is already in order, the list comes out in a valid dependency order directly (no reversal needed).
class Solution: def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]: graph = [[] for _ in range(numCourses)] for a, b in prerequisites: graph[a].append(b)
UNVISITED, VISITING, DONE = 0, 1, 2 state = [UNVISITED] * numCourses order = []
def dfs(u): if state[u] == VISITING: return False if state[u] == DONE: return True
state[u] = VISITING for v in graph[u]: if not dfs(v): return False state[u] = DONE order.append(u) return True
for u in range(numCourses): if not dfs(u): return [] return orderTracing on the example, looping dfs(0) through dfs(3):
dfs(0): no dependencies. Append 0.order = [0].dfs(1): depends on 0, alreadyDONE. Append 1.order = [0, 1].dfs(2): depends on 0, alreadyDONE. Append 2.order = [0, 1, 2].dfs(3): depends on 1 and 2, both alreadyDONE. Append 3.order = [0, 1, 2, 3].
Why itβs correct: a node is appended to order only after every node it depends on is already in order (either appended earlier or confirmed DONE in this call), so by induction every dependency precedes its dependent in the final list. Complexity: each node is visited once and each edge is followed once β O(V + E) time, O(V + E) space for the graph, color array, and recursion stack.
Kahn's Algorithm (BFS Topological Sort)
OptimalTime O(V + E)Space O(V + E)Build the graph pointing prerequisite β dependent, track in-degrees (number of unmet prerequisites), and repeatedly dequeue courses with in-degree 0 β courses that are ready to take right now β appending each to the result as itβs dequeued.
from collections import deque
class Solution: def findOrder(self, numCourses: int, prerequisites: list[list[int]]) -> list[int]: graph = [[] for _ in range(numCourses)] indegree = [0] * numCourses for a, b in prerequisites: graph[b].append(a) indegree[a] += 1
queue = deque(u for u in range(numCourses) if indegree[u] == 0) order = []
while queue: u = queue.popleft() order.append(u) for v in graph[u]: indegree[v] -= 1 if indegree[v] == 0: queue.append(v)
return order if len(order) == numCourses else []Play through the same example β n = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]. Watch the in-degree badges feed the ready queue, and the queue feed the order below β every pop is one course locked into the answer:
Courses(badge = prerequisites still needed)
Prerequisite edges (left unlocks right)
Queue β front on the left, next to pop
Order so far β 0 / 4 courses popped
Counting phase: each prerequisite pair adds one point to the in-degree of the course that depends on it β the badges that will tell us how many prerequisites a course still needs. The pairs are [1,0], [2,0], [3,1], [3,2]: course 1 needs 0, course 2 needs 0, course 3 needs both 1 and 2. The chips above show the same edges in unlock direction, so watch course 3 (hit by two pairs) climb the furthest.
A cycle flips the answer to [] β a queue that runs dry without finishing every course is a queue that got stuck. On n = 3, prerequisites = [[1,0],[2,1],[1,2]], courses 1 and 2 wait on each other. Watch the queue seed, pop course 0, and then die:
Courses(badge = prerequisites still needed)
Prerequisite edges (left unlocks right)
Queue β front on the left, next to pop
Order so far β 0 / 3 courses popped
Same algorithm, now on a cyclic graph: 3 courses with pairs [1,0], [2,1], and [1,2]. Pair [1,0]: course 1 needs course 0, so indegree[1] becomes 1.
Why itβs correct: a course reaches in-degree 0 exactly when every course it depends on has already been dequeued (placed earlier in order), so appending it at that moment preserves every dependency constraint. If a cycle exists, every course in it keeps at least one un-decremented in-degree forever, so order never reaches length numCourses, correctly signaling impossibility. Complexity: each node is enqueued once and each edge decrements one counter β O(V + E) time, O(V + E) space. Same complexity as the DFS version, but iterative β no recursion depth to worry about on graphs with long chains, and the ready-to-take intuition maps directly onto how youβd actually plan the courses.