Definition
- DFS from each unvisited node – explore all reachable descendants before recording a node.
- Post-order recording – push a node onto a stack only after all its descendants are fully explored.
- Reverse to get order – the reversed stack is a valid topological order.
- Three-color cycle detection – white (unvisited), gray (in progress), black (finished); a gray-to-gray edge is a cycle.
- O(V + E) time – each node and edge is visited once.
Think of resolving dependency chains for report generation at National Bank of India. Pankaj dives down each dependency chain to its deepest point, records stages as they finish, and when he reverses that finish order he gets a sequence where every dependency comes before the stage needing it.
Understanding the Concepts
What is the problem?
Produce a topological order of a DAG using depth-first search. In a banking context, a report-generation pipeline at National Bank of India has stages that depend on upstream stages, and Pankaj needs them in dependency order.
How does DFS topological sort work?
For each unvisited node, run DFS that first recurses into all successors, then records the node after those recursions return (post-order). Push each finished node onto a stack. After all nodes are processed, pop the stack to get the topological order.
package com.bank.service;
import java.util.*;
public class ReportPipelineScheduler {
private static final int WHITE = 0, GRAY = 1, BLACK = 2;
public static List<Integer> topologicalSort(int numStages, int[][] deps) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numStages; i++) adj.add(new ArrayList<>());
for (int[] d : deps) adj.get(d[0]).add(d[1]);
int[] color = new int[numStages];
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < numStages; i++) {
if (color[i] == WHITE) {
dfs(i, adj, color, stack);
}
}
List<Integer> order = new ArrayList<>();
while (!stack.isEmpty()) order.add(stack.pop());
return order;
}
private static void dfs(int u, List<List<Integer>> adj,
int[] color, Deque<Integer> stack) {
color[u] = GRAY;
for (int v : adj.get(u)) {
if (color[v] == GRAY) {
throw new IllegalStateException("Cycle detected at node " + v);
}
if (color[v] == WHITE) {
dfs(v, adj, color, stack);
}
}
color[u] = BLACK;
stack.push(u);
}
public static void main(String[] args) {
// 0=Load data, 1=Validate, 2=Transform, 3=Aggregate, 4=Render
int[][] deps = {{0,1}, {0,2}, {1,3}, {2,3}, {3,4}};
List<Integer> schedule = topologicalSort(5, deps);
String[] names = {"Load data", "Validate", "Transform",
"Aggregate", "Render"};
System.out.println("Report pipeline order:");
for (int s : schedule) {
System.out.println(" " + s + ": " + names[s]);
}
}
}
Report pipeline order:
0: Load data
2: Transform
1: Validate
3: Aggregate
4: Render
Why reverse the post-order?
DFS finishes descendants before ancestors, so the raw post-order places each node after its successors. Reversing this puts each node before its successors, which is the topological order.
How do the three colors detect cycles?
White means unvisited, gray means currently on the recursion path, and black means fully explored. If DFS reaches a gray node, that is a back edge forming a cycle. A black node reached from a different path is not a cycle: it was just already explored.
if (color[v] == GRAY) {
// Back edge: v is on current path → cycle
throw new IllegalStateException("Cycle detected");
}
if (color[v] == BLACK) {
// Cross/forward edge: v already finished → safe, skip
}
What is the time and space complexity?
- Time: O(V + E) because each node and edge is visited once.
- Space: O(V) for the color array, recursion stack, and output stack.
How is this different from Kahn's algorithm?
| Aspect | DFS-based | Kahn's (BFS) |
|---|---|---|
| Approach | Post-order reversal | Zero-in-degree removal |
| Cycle detection | Gray state back edge | Emitted count < total |
| Recursion | Yes (or explicit stack) | No, naturally iterative |
| Lex-smallest order | Not straightforward | Use PriorityQueue |
What are common mistakes?
The most common mistake is using a single boolean visited array instead of three colors, which cannot distinguish a back edge (cycle) from a cross edge (safe). Another mistake is forgetting to reverse the post-order.
// Wrong: single visited flag cannot detect cycles correctly
if (visited[v]) continue; // may skip a real cycle
// Correct: three colors distinguish in-progress from finished
if (color[v] == GRAY) throw new IllegalStateException("Cycle");
What about deep graphs and stack overflow?
For very deep graphs the recursion can overflow the call stack. Convert to an iterative DFS using an explicit stack to avoid this. Kahn's algorithm is naturally iterative and avoids this concern entirely.
What should a senior engineer know?
A senior engineer knows that the DFS approach can produce different valid orderings depending on the traversal order of successors, and that neither DFS nor Kahn's guarantees a unique order for a general DAG. The senior also knows that DFS topological sort is the basis for strongly connected component algorithms like Tarjan's and Kosaraju's.
Visual Overview
DFS traversal (starting from node 0):
dfs(0) → color[0]=GRAY
dfs(1) → color[1]=GRAY
dfs(3) → color[3]=GRAY
dfs(4) → color[4]=GRAY
no successors → color[4]=BLACK, push 4
color[3]=BLACK, push 3
color[1]=BLACK, push 1
dfs(2) → color[2]=GRAY
dfs(3) → color[3]=BLACK (skip)
color[2]=BLACK, push 2
color[0]=BLACK, push 0
Stack (top to bottom): [0, 2, 1, 3, 4]
Pop order (topological): 0, 2, 1, 3, 4
Key Takeaway
DFS-based topological sort runs depth-first search from each unvisited node and pushes a node onto a stack only after all of its descendants have been fully explored. Because a node is recorded only after everything it points to, the post-order places each node after its successors, so reversing gives the topological order.
The three-color scheme is essential for cycle detection. White for unvisited, gray for nodes on the current recursion path, and black for finished. A gray-to-gray edge is a back edge forming a cycle. The gray versus black distinction prevents false positives from cross edges.
In my banking project at National Bank of India, a report-generation pipeline had stages depending on upstream stages, and they had to run in dependency order. DFS post-order produced that order by finishing each stage's dependencies before recording it.
For very deep graphs I convert the recursion to an explicit stack to avoid overflow, or I use Kahn's algorithm which is naturally iterative.
Remember one sentence: DFS finishes descendants first, so reversing the post-order gives the topological order, and three colors detect cycles.
---