Topological sort (DFS)
Order tasks so each comes after its requirements by running DFS and listing each task only after everything it leads to is finished.
O(n + e)Space O(n + e)3 practice problemsStep-through pictureThe idea
This is a second way to find a topological order: an order of tasks where every edge from → to has from before to. It uses depth-first search instead of in-degree counts.
The key fact: when DFS finishes a node (all of its outgoing edges are fully explored), every node it leads to has already finished. So if you write nodes down in the order they finish, every node appears after the nodes it points to. Reverse that list and every node appears before the nodes it points to, which is a topological order.
To catch cycles, each node has three states: new, in progress (on the current DFS path), and done. Meeting an in-progress node means you walked in a circle back to your own path, so no order exists.
An everyday example
Writing a recipe backwards
To bake a cake you need frosting and a baked sponge; the sponge needs batter; the batter needs eggs and flour. Starting from the cake, you follow each requirement as deep as it goes. You can only write a step down once all of its requirements are written. Reading your notes from bottom to top would give the cake first, so you flip the list over to get the order to actually cook in.
| In the example | In the code |
|---|---|
| Following a requirement deeper | Calling visit(next) |
| A step you are still working out | state[node] === 1, in progress |
| Writing a step down once its needs are written | order.push(node) after the loop |
| Flipping the notes over | order.reverse() |
From the slow way to topological sort (dfs)
- Suppose you already know DFS and want a dependency order from it. The first idea is to list nodes in the order DFS enters them. That fails: with edges
0 → 1and2 → 1, entering from0gives[0, 1, 2], and1comes before2even though2must come first. - Look at when DFS finishes a node instead, meaning the moment
visit(node)returns. By then, every node it can reach has been visited and has finished too. So if you write nodes down as they finish, each node is written after every node it leads to. - That is backward from what you want, but only backward. Reverse the finish list and each node comes before every node it leads to, which is exactly a topological order. So push a node after its neighbor loop, and reverse
orderat the end. - Two details finish it. A DFS from one node may not reach everything, so start one from every node. And a cycle must be caught, which needs to tell "on my current path" apart from "finished earlier": three states instead of a visited flag. That is topological sort with DFS.
How it works
- Build
graphfrom the edges and set everystateto 0 (new). - For each node from
0ton - 1, callvisit(node). - In
visit, if the node is in progress (state1), you found a cycle: returnfalse. If it is done (state2), returntrueright away. - Mark the node in progress and visit every neighbor. If any visit finds a cycle, pass the
falseup. - After all neighbors are finished, mark the node done and push it to
order. - At the end, reverse
order. Every node now comes before every node it points to.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function topoSortDfs(n, edges) {const graph = Array.from({ length: n }, () => []);for (const [from, to] of edges) graph[from].push(to);const state = new Array(n).fill(0);const order = [];function visit(node) {if (state[node] === 1) return false;if (state[node] === 2) return true;state[node] = 1;for (const next of graph[node]) {if (!visit(next)) return false;}state[node] = 2;order.push(node);return true;}for (let node = 0; node < n; node++) {if (!visit(node)) return [];}return order.reverse();}
How fast it is
| Time | O(n + e) |
|---|---|
| Extra memory | O(n + e) |
Each node is visited once and each edge is followed once. The graph, state, order, and the call stack together hold O(n + e) entries.
- Best case
O(n)There are no edges. Each outer call marks a node, finds no neighbors, and finishes it.
- Average case
O(n + e)Each node gets one real visit, and each edge causes one call, which may return at once.
- Worst case
O(n + e)Every node and edge is explored, which happens whenever there is no cycle.
Common mistakes
- Pushing a node to
orderbefore visiting its neighbors. That gives a preorder, which is not a valid dependency order. - Forgetting to reverse
orderat the end, so every dependency comes after the thing that needs it. - Using a single
visitedflag. It reports a cycle whenever two paths meet at the same node, even without a loop. - Only calling
visitfrom node 0, missing nodes that can't be reached from it.
When to use it
Look for these clues in a problem:
- You need a dependency order, and you are already doing DFS on the graph.
- The question also asks for something computed from each node's descendants, such as the longest chain below it.
- You need to detect a dependency loop at the same time as ordering.
- The graph is given as an adjacency list and recursion depth is small enough.
Check yourself
Answer each one in your head first, then open it.
Why push a node after its neighbor loop and not before?
After the loop, every node it leads to has already been pushed, so the list has the right shape to reverse. Pushing before gives entering order, which breaks examples like 0 → 1 with 2 → 1.
In the trace, why didn't visit(4) from 1 push 4 a second time?
4 was already in state 2, done, so the call returned true at once. Each node is pushed exactly once.
What does finding a node in state 1 mean?
That node is on the current chain of calls, so there is a path from it to the current node. The edge back to it closes a cycle.
How deep can the recursion go?
Up to n calls, on a single chain like 0 → 1 → 2 → .... Every node on the chain is in progress at the same time.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Take any edge a → b at the moment visit(a) looks at it, while a is in progress. There are three cases. If b is new, visit(b) runs and finishes completely before a can finish. If b is done, it was pushed already. Either way, b is pushed before a, so after reversing, a comes before b.
The third case is b in progress. The nodes in progress are exactly the current chain of calls, so b is an ancestor of a on the path: there is a path b → ... → a, and with the edge a → b that is a cycle. The code returns false, and no order exists anyway. So when the graph has no cycle, every edge falls into the first two cases and the reversed list is a valid order.
When the graph does have a cycle, it is always caught. Let x be the first node of the cycle to be marked in progress. Every other node on the cycle is still new and reachable from x, so it is visited before x finishes. That includes the node just before x on the cycle, whose edge then finds x in progress. It all ends because each node is marked in progress once and each edge is followed once.
Going deeper
3 more ideas
Why reversed finish order works
Take any edge a → b. If b was visited first, it finished before a even started. If a was visited first, then visit(a) calls visit(b) (or b is already finishing inside it), so b finishes before a. Either way b is pushed before a, and after reversing, a comes before b. The only failure is when b is still in progress, which means there is a cycle.
Why three states, not two
A plain visited flag can't tell "done earlier" from "on my current path". In the example, 3 is reached twice, from 1 and from 2, but that is not a cycle: by the second time, 3 is done. Only reaching a node that is still in progress (an ancestor on the current path) means there is a loop.
Deep graphs
This version uses recursion, so a chain of tens of thousands of nodes can overflow the call stack. For huge graphs, use Kahn's algorithm, which needs no recursion, or write the DFS with an explicit stack.
Work it by hand
Every step in a table, with why
Order 5 tasks, where an edge [a, b] means a must come before b.
n = 5, edges = [[3, 0], [0, 2], [0, 4], [1, 4], [3, 1]]
| Step | Step | node | state | order | Why |
|---|---|---|---|---|---|
| 1 | Outer loop calls visit(0) | 0 | [1, 0, 0, 0, 0] | [] | 0 is new, so it is marked 1, in progress. Its edges go to 2 and 4. |
| 2 | visit(2) | 2 | [1, 0, 1, 0, 0] | [] | 2 is new. It is marked in progress, on the path 0 → 2. |
| 3 | Finish 2 | 2 | [1, 0, 2, 0, 0] | [2] | 2 has no outgoing edges, so it is done first and pushed first. |
| 4 | visit(4) | 4 | [1, 0, 2, 0, 1] | [2] | Back in visit(0), the next edge leads to 4, which is new. |
| 5 | Finish 4 | 4 | [1, 0, 2, 0, 2] | [2, 4] | 4 has no outgoing edges either. |
| 6 | Finish 0 | 0 | [2, 0, 2, 0, 2] | [2, 4, 0] | Both of 0's targets are done, so 0 is done and pushed after them. |
| 7 | Outer loop calls visit(1) | 1 | [2, 1, 2, 0, 2] | [2, 4, 0] | 1 was never reached from 0, so the outer loop starts it. |
| 8 | visit(4) from 1 | 4 | [2, 1, 2, 0, 2] | [2, 4, 0] | 4 is already done (state 2), so it returns true at once. Reaching a done node twice is not a cycle. |
| 9 | Finish 1 | 1 | [2, 2, 2, 0, 2] | [2, 4, 0, 1] | 1 is pushed after 4, the node it points to. |
| 10 | Outer loop: visit(2) | 2 | [2, 2, 2, 0, 2] | [2, 4, 0, 1] | 2 is done, so this call returns at once. |
| 11 | Outer loop calls visit(3) | 3 | [2, 2, 2, 1, 2] | [2, 4, 0, 1] | 3 is new. Its edges lead to 0 and 1, both already done. |
| 12 | Finish 3 | 3 | [2, 2, 2, 2, 2] | [2, 4, 0, 1, 3] | 3 is pushed last, after every node it leads to. The outer loop's call on 4 returns at once. |
| 13 | order.reverse() | - | all 2 | [3, 1, 0, 4, 2] | Reversing the finish order puts every node before the nodes it points to. |
It returns [3, 1, 0, 4, 2]. Check each rule: 3 is before 0 and 1, 0 is before 2 and 4, and 1 is before 4. Kahn's algorithm on the same graph gives [3, 0, 1, 2, 4], which is also valid.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Topological sort (DFS) | O(n + e) | O(n + e) | You are already walking the graph with DFS, need values from each node's descendants, or want the cycle itself. |
| Kahn's algorithm | O(n + e) | O(n + e) | The graph may be very deep, you want rounds of parallel work, or you need a tie rule such as smallest-first. |
| Trying every order | O(n! · e) | O(n) | Only to check answers in tests with 8 or fewer nodes. |
Extra memory: O(n + e): the adjacency list holds the edges, state and order hold n entries, and the call stack can be n calls deep on a long chain.
How the steps grow
- Topological sort (DFS), Kahn's algorithm
O(n + e) - Trying every order
O(n! · e)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Topological sort (DFS) | 20 | 2,000 | 2 million |
| Kahn's algorithm | 20 | 2,000 | 2 million |
| Trying every order | 36 million | more than a quadrillion | more than a quadrillion |
Inputs that need care
5 tricky inputs
- No edges:
n = 4, edges = [] - Nodes finish in the order
0, 1, 2, 3, so the result is[3, 2, 1, 0]. Any order is valid when there are no rules. - A self-loop:
[2, 2] visit(2)marks2in progress, follows the edge to itself, finds state 1, and returnsfalse. The result is[].- A cycle that node 0 can't reach
- The outer loop reaches it later and the in-progress check catches it then. This is why the loop starts from every node.
- A chain of 200,000 tasks
- The recursion goes 200,000 calls deep and throws "Maximum call stack size exceeded". Use Kahn's algorithm or an explicit stack for very deep graphs.
- A cycle found halfway through
- Some nodes are left in state 1 when
falsecomes back. That is fine because the function returns[]at once, but resetstateif you reuse it for another search.
Variations and follow-ups
Twists interviewers add
Explicit stack
Store [node, index of next neighbor] pairs on a stack. Look at the top: if it has another neighbor, move to it; if not, pop it and push the node to order. That is the same post-order with no recursion limit.
Longest chain of tasks
Go through the finished order and compute, for each node, 1 plus the longest chain among the nodes it points to. Because those nodes finished first, their values are ready. This gives the critical path of a project.
Reporting the cycle
Keep a stack of the nodes in progress. When an edge reaches a node in state 1, the part of the stack from that node to the top is the cycle, ready to show in an error message.
Building the order without reversing
Fill an array of size n from the end, putting each finished node at the last free position. The result is already in the right order. Avoid unshift, which moves every element each time.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from dependency words, the same as Kahn's algorithm. Pick this version when the problem also needs something from each node's descendants, or when you are already writing a DFS.
- Say the key fact first: "a node finishes only after everything it leads to, so reversed finish order is a valid order". Then explain the three states.
- Test by hand: a diamond like
0 → 1,0 → 2,1 → 3,2 → 3(not a cycle), a self-loop, and a cycle not reachable from node 0. - Expect "why not just a visited flag?". In the diamond,
3is reached twice; only the third state tells "finished earlier" apart from "on my current path".
Where frontend code uses it
The same idea in real UI code
- ES module loading evaluates a module only after the modules it imports have been evaluated, which follows a depth-first post-order walk of the import graph.
- Bundlers walk import statements depth-first from an entry file and can emit modules in post-order so dependencies are defined first.
- Design systems with tokens that reference other tokens (a button color that points to a brand color) resolve the referenced tokens first with a DFS.