CMD Guide
HomeDSAGraphs

Topological Sort

Topological sort works because a directed acyclic graph (DAG) has at least one node with in-degree zero at every stage of peeling — repeatedly stripping off a source (a node with no unprocessed prerequisites) and recording it produces a linear order where every edge u -> v has u appearing before v; this is only possible when the graph has no cycles, since a cycle would leave every node in it permanently waiting on another.

Recognize the pattern

Brute force vs. the real algorithm

Naive baseline: repeatedly scan all V nodes, find any node whose prerequisites are already placed, place it, remove it, and restart the scan from the top. Each full scan costs O(V + E) (check every node's incoming edges), and you repeat the scan V times → O(V · (V + E)) total. It works but rediscovers the same in-degree information from scratch every round.

Kahn's algorithm (BFS-flavored, in-degree counting) and the DFS finish-time algorithm both keep running state so every node and edge is examined a constant number of times overall, dropping the cost to linear.

Kahn's algorithm (BFS on in-degrees)

  1. Compute in-degree[v] for every node (count of incoming edges).
  2. Push all nodes with in-degree 0 into a queue — these have no unmet prerequisite.
  3. Pop a node u, append it to the result order.
  4. For each edge u→v, decrement in-degree[v]. If it hits 0, push v.
  5. Repeat until the queue is empty. If the result has fewer than V nodes, a cycle exists among the leftover nodes — no valid order.
import java.util.*;

public class TopoKahn {
    // returns topological order, or empty list if a cycle exists
    public static List<Integer> topoSort(int n, List<List<Integer>> adj) {
        int[] indeg = new int[n];
        for (int u = 0; u < n; u++)
            for (int v : adj.get(u)) indeg[v]++;

        Deque<Integer> queue = new ArrayDeque<>();
        for (int v = 0; v < n; v++)
            if (indeg[v] == 0) queue.add(v);

        List<Integer> order = new ArrayList<>();
        while (!queue.isEmpty()) {
            int u = queue.poll();
            order.add(u);
            for (int v : adj.get(u)) {
                if (--indeg[v] == 0) queue.add(v);
            }
        }
        if (order.size() != n) return new ArrayList<>(); // cycle detected
        return order;
    }
}

DFS finish-time algorithm

Run DFS from every unvisited node. When a node has no more unexplored outgoing edges (it "finishes"), push it onto a stack. Because a node only finishes after all nodes reachable from it have already finished, reversing the finish order (or popping the stack) yields a valid topological order. Cycle detection needs a third color: a node currently on the recursion stack (gray) reached again means a back-edge → cycle.

import java.util.*;

public class TopoDFS {
    private static final int WHITE = 0, GRAY = 1, BLACK = 2;

    public static List<Integer> topoSort(int n, List<List<Integer>> adj) {
        int[] color = new int[n];
        Deque<Integer> finishStack = new ArrayDeque<>();
        for (int v = 0; v < n; v++) {
            if (color[v] == WHITE) {
                if (dfs(v, adj, color, finishStack)) {
                    return new ArrayList<>(); // cycle detected
                }
            }
        }
        List<Integer> order = new ArrayList<>();
        while (!finishStack.isEmpty()) order.add(finishStack.pop());
        return order;
    }

    // returns true if a cycle is detected
    private static boolean dfs(int u, List<List<Integer>> adj, int[] color, Deque<Integer> finishStack) {
        color[u] = GRAY;
        for (int v : adj.get(u)) {
            if (color[v] == GRAY) return true; // back-edge -> cycle
            if (color[v] == WHITE) {
                if (dfs(v, adj, color, finishStack)) return true;
            }
        }
        color[u] = BLACK;
        finishStack.push(u); // record finish time
        return false;
    }
}

Why the thread safety matters: In real production backend code, topological sorting (e.g., resolving dependencies in a request handler) is executed concurrently across multiple threads. Using static/shared mutable state like `hasCycle` causes data races that silently corrupt results or report false cycles. By passing the state through local variables and returning status directly from recursive frames, the code becomes completely thread-safe.

Recursion Safety in Production: Stack vs. Heap

While the recursive DFS implementation above is mathematically elegant and simple to write, it carries a severe operational risk: Call Stack Exhaustion. Recursive functions allocate a stack frame on the OS call stack (which defaults to a tight limit, e.g., 1MB in Java). If the dependency graph is highly linear or skewed with a depth exceeding roughly 10,000 nodes, running recursive DFS will crash the application thread with a StackOverflowError.

To write production-safe code, we must migrate the call stack frames to the heap by using an explicit stack collection (e.g., Deque). This allows the traversal to scale up to the limits of available system RAM (gigabytes) rather than the call stack limit (megabytes).

import java.util.*;

public class TopoDFSIterative {
    private static final int WHITE = 0, GRAY = 1, BLACK = 2;

    public static List<Integer> topoSort(int n, List<List<Integer>> adj) {
        int[] color = new int[n];
        Deque<Integer> finishStack = new ArrayDeque<>();
        
        for (int start = 0; start < n; start++) {
            if (color[start] != WHITE) continue;
            
            Deque<Integer> stack = new ArrayDeque<>();
            stack.push(start);
            
            while (!stack.isEmpty()) {
                int u = stack.pop();
                if (color[u] == WHITE) {
                    color[u] = GRAY;
                    stack.push(u); // push back to color black when all neighbors finish
                    for (int v : adj.get(u)) {
                        if (color[v] == GRAY) return new ArrayList<>(); // cycle detected
                        if (color[v] == WHITE) {
                            stack.push(v);
                        }
                    }
                } else if (color[u] == GRAY) {
                    color[u] = BLACK;
                    finishStack.push(u);
                }
            }
        }
        
        List<Integer> order = new ArrayList<>();
        while (!finishStack.isEmpty()) {
            order.add(finishStack.pop());
        }
        return order;
    }
}

Complexity, derived from first principles

Kahn's algorithm: building in-degrees scans every edge once → O(E). Each node is pushed to the queue exactly once (when its in-degree first hits 0) and popped exactly once → O(V) queue operations. Each edge is examined exactly once when its source is popped (to decrement the target's in-degree) → O(E). Total time O(V + E). Space: in-degree array O(V), queue holds at most V nodes O(V), result list O(V) → O(V) auxiliary space (plus the O(V+E) adjacency list itself).

DFS algorithm: each node is visited (colored gray then black) exactly once, and the inner loop over each node's outgoing edges runs once per node → total edge examinations across all calls = O(E), total node visits = O(V). Total time O(V + E). Space: color array O(V), recursion stack up to O(V) in the worst case (a long chain), finish stack O(V) → O(V) auxiliary space.

Both are asymptotically identical; the constant-factor difference is recursion overhead (DFS) vs. explicit queue management (Kahn's).

Traced worked example

Graph (6 build targets 0–5) with edges: 5→2, 5→0, 4→0, 4→1, 2→3, 3→1. Read "5→2" as "5 must build before 2".

Kahn's trace

Initial in-degrees: 0:2, 1:2, 2:1, 3:1, 4:0, 5:0.

StepPopOrder so farUpdatesQueue after
0[]seed queue with in-degree-0 nodes[4, 5]
14[4]indeg[0]: 2→1, indeg[1]: 2→1[5]
25[4,5]indeg[2]: 1→0 (push 2), indeg[0]: 1→0 (push 0)[2,0]
32[4,5,2]indeg[3]: 1→0 (push 3)[0,3]
40[4,5,2,0]no outgoing edges[3]
53[4,5,2,0,3]indeg[1]: 1→0 (push 1)[1]
61[4,5,2,0,3,1]no outgoing edges[]

Result has all 6 nodes → valid order 4, 5, 2, 0, 3, 1.

DFS trace

Visit nodes in id order 0..5, pushing to a finish-stack on completion.

  1. DFS(0): no out-edges → finish 0. Stack: [0]
  2. DFS(1): no out-edges → finish 1. Stack: [0,1]
  3. DFS(2): visits 3 first (3→1 already black, skip) → finish 3, then finish 2. Stack: [0,1,3,2]
  4. DFS(3): already black, skip.
  5. DFS(4): edges to 0 (black) and 1 (black), nothing new → finish 4. Stack: [0,1,3,2,4]
  6. DFS(5): edges to 2 (black) and 0 (black) → finish 5. Stack: [0,1,3,2,4,5]

Popping the stack gives order 5, 4, 2, 3, 1, 0 — a different but equally valid topological order (both satisfy every edge constraint; topological order is not unique).

Pitfalls

When to use / when not, and trade-offs

Use topological sort when: you have a DAG of dependencies and need any valid linear execution order, or you need to detect whether a cyclic dependency makes the task impossible (circular build dependency, deadlocked course prerequisites, a cyclic import graph).

Do not use it when: edges have weights and you need the cheapest/longest path through the DAG — topo order is a building block for DAG shortest/longest path (process nodes in topo order, relax edges once each), not a replacement for it. Also skip it if the graph is undirected, or if you need all valid orderings (that requires backtracking, not a single linear pass).

ApproachTimeSpaceCycle detectionStyle
Kahn's (in-degree + queue)O(V+E)O(V)leftover nodes after queue emptiesiterative, easy to explain, gives BFS-like "levels" for free
DFS finish-timeO(V+E)O(V)gray node revisited (back-edge)recursive, natural if you already have DFS infrastructure; risk of stack overflow on very deep/skewed graphs
Naive repeated scanO(V·(V+E))O(V)no progress in a full passsimplest to write, too slow beyond small inputs

Prefer Kahn's when you also want to process nodes level-by-level (e.g. parallel task scheduling: everything in one queue-pop "wave" can run concurrently) or when recursion depth is a concern. Prefer DFS-based when you're already running DFS for other reasons (e.g. computing SCCs via Tarjan/Kosaraju, which uses the same finish-time idea).

🤖 Don't fully get this? Learn it with Claude

Stuck on Topological Sort? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Topological Sort** (DSA) and want to truly understand it. Explain Topological Sort from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Topological Sort** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Topological Sort** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Topological Sort** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes