Topological Sort
Quick reference
| DFS-based topological sort | O(V + E) |
| Kahn's algorithm | O(V + E) |
Getting dressed has rules, but not a full order. Socks come before shoes. Underwear comes before pants. But underwear and socks can go on in either order โ nothing forces one before the other. A topological sort is exactly that: an arrangement of items where every dependency comes before the thing depending on it, while anything with no relationship to each other can land in any order, and multiple correct arrangements can exist side by side.
Only one shape of graph can be arranged this way at all: directed, and with no cycle โ a DAG, short for 'directed acyclic graph.' Directed, because 'before' only makes sense if edges have a direction โ an edge from socks to shoes means socks-before-shoes, not the reverse. Acyclic, because a cycle breaks the whole premise: if A must come before B, B before C, and C before A, there is no starting point โ every candidate for 'first' has something behind it demanding to go earlier still. No valid order exists, and that has to be detected, not silently ignored.
Two ways to the same order
structure Graph (adjacency list, directed):
adj: map from node -> list of nodes it points TO (its dependents)
structure InDegree:
inDegree: map from node -> count of edges pointing INTO it
// a node with inDegree 0 has nothing left it's waiting onDFS-based โ finish a node, then push it; reverse at the end
function topoSortDFS(nodes, adj):
visited = {}
stack = [] // built in FINISH order, not visit order
function dfs(node):
if node in visited:
return
visited.add(node)
for neighbor in adj[node]:
dfs(neighbor)
stack.push(node) // every dependent already pushed โ node is done
for node in nodes:
dfs(node)
return reverse(stack)Kahn's algorithm โ BFS-based, using in-degree
function topoSortKahn(nodes, adj):
inDegree = {node: 0 for node in nodes}
for node in nodes:
for neighbor in adj[node]:
inDegree[neighbor] += 1
queue = Queue()
for node in nodes:
if inDegree[node] == 0: // nothing left it's waiting on โ safe to start
enqueue(queue, node)
result = []
while queue is not empty:
node = dequeue(queue)
result.append(node)
for neighbor in adj[node]:
inDegree[neighbor] -= 1 // node is done โ one less thing neighbor waits on
if inDegree[neighbor] == 0:
enqueue(queue, neighbor)
return resultBoth approaches are just a structured single pass over the graph โ no extra searching, no revisiting. Kahn's builds and drains an in-degree map once; the DFS version runs one DFS over every node once. Neither does more work than a plain traversal already would.
| Operation | Time | Space | Why |
|---|---|---|---|
| DFS-based topological sort | O(V + E) | O(V) | Standard DFS traversal cost, plus a stack that holds each node exactly once โ no node is pushed twice. |
| Kahn's algorithm | O(V + E) | O(V) | Building in-degree counts is O(V + E); every node enters and leaves the queue exactly once, and every edge is examined exactly once to decrement a count. |
Running it on a graph that actually has a cycle
The DFS version will still produce some order without crashing โ it just won't be valid, because no valid order exists. Kahn's algorithm gives a cleaner signal: if a cycle exists, every node inside that cycle is permanently stuck waiting on another node in the same cycle, so its in-degree never reaches zero and it's never enqueued. result ends up shorter than nodes. Checking len(result) == len(nodes) at the end โ not just returning result and trusting it โ is what turns 'silently wrong' into 'correctly reports impossible.'
Applying this to an undirected graph
'Before' has no meaning without direction โ an undirected edge between A and B says they're connected, not that one comes first. In-degree isn't even well-defined on an undirected graph the way Kahn's algorithm needs it. If a problem hands you undirected edges and asks for an ordering, something upstream is wrong with the setup, not the algorithm.
Assuming there's exactly one valid order
Socks-then-pants-then-shoes and pants-then-socks-then-shoes are both valid if nothing constrains socks relative to pants directly. Whenever Kahn's queue holds more than one node at once, either could go next and the choice changes the final order without making it wrong. A checker for this kind of problem has to verify the ordering property (every edge points from earlier to later in the result) rather than compare against one fixed expected sequence.
Forgetting the DFS version's final reverse
The stack in topoSortDFS() is built in finish order: a node is pushed only after everything it points to has already finished and been pushed. That means dependents end up below their dependencies on the stack, not above โ the raw stack reads dependents-first. It looks like a real ordering (it has the right nodes, it just runs backwards), which makes it an easy bug to miss until it's checked against an actual dependency edge.
Sign in to mark problems done โ progress syncs across devices.