29

Topological Sort

Put tasks in an order where every task comes after the tasks it depends on.

intermediate~60 min4 simulations9 problems

▶ guided lesson⚡ 1-minute summary↻ 13 recall cards

before this: Graphs · Queues · Heaps · Recursion

01 · why it exists

Why this exists, and the weight it carries

since1962

Topological sort with Kahn's algorithm

A. B. Kahn (Westinghouse Electric Corporation, Baltimore)

In the late 1950s, big projects such as the US Navy's Polaris missile programme were planned as PERT networks: thousands of events, each waiting for others. Before a computer could analyse such a network, the events had to be listed so that every event came after the ones it waits for. In 1962 A. B. Kahn published "Topological sorting of large networks" in Communications of the ACM. He estimated that, on an IBM 7090, a 30,000-activity PERT network could be ordered in under an hour of machine time — though, as he noted, the method had not yet been programmed.

the problem it solvedPutting thousands of dependent events in a valid order — and noticing when the dependencies loop.

its jobs today

  • Build tools: Gradle builds a directed acyclic graph of tasks before running any of them; Maven's reactor sorts modules so each is built before it is needed.
  • Spreadsheets: Excel turns formula dependencies into a calculation chain — the order cells must be calculated in — and warns about circular references.
  • Workflow schedulers: an Apache Airflow DAG decides which task may start once its upstream tasks are done.

the weight it carries

Illustrative: a build with 2,000 modules and 10,000 dependency edges. Kahn: 2,000 + 10,000 = 12,000 steps to get a build order or report a cycle. Rescanning for a free module every round instead: up to 2,000 rounds × 12,000 = 2.4·10⁷ steps.

without it

Builds, spreadsheets and schedulers would have to guess an order and retry, and a circular dependency would hang instead of failing with a clear error.

when · why · how

when
Prerequisites, dependencies, a build order — or "is it possible at all?"
why
A vertex with in-degree 0 has nothing left to wait for, so it can go next. Each vertex is polled once and each edge decremented once: O(V + E), no recursion, and a free cycle check (output count < V).
how
int[] indegree, List<List<Integer>> adj and an ArrayDeque<Integer> queue; swap in a PriorityQueue<Integer> for the smallest order.

02 · plain english

The idea in 30 seconds

Draw an arrow u → v for "u must happen before v". A topological order lists every vertex so all arrows point forward.

Kahn: repeatedly take a vertex with no arrows coming in (nothing left to wait for). DFS: a vertex is finished only after everything it points to — so reverse the finish order.

It exists only if there is no directed cycle. Both methods detect the cycle for free — that is half of all interview questions on this topic.

picture this

A Gradle or Maven build: a module compiles only after the modules it depends on. The build tool keeps picking a module whose dependencies are already built, builds it, and repeats. If module A needs B and B needs A, nothing can ever start — the build stops with a cycle error.

03 · remember

What you must know

  • A DAG (directed acyclic graph) always has at least one topological order; a graph with a directed cycle has none.
  • Kahn's algorithm (BFS): compute in-degrees, queue every in-degree-0 vertex, pop → output → decrement neighbours → enqueue new zeros. O(V + E).
  • Cycle check with Kahn: if fewer than V vertices were output, the rest are stuck on (or behind) a cycle.
  • DFS method: three colours — white (unvisited), gray (on the current path), black (finished). Push a vertex when it turns black; the reversed push order is a topological order.
  • Cycle check with DFS: an edge into a gray vertex is a back edge = a cycle. An edge into a black vertex is harmless.
  • Orders are usually not unique. Need the lexicographically smallest one? Swap Kahn's queue for a PriorityQueue → O(V log V + E). Unique order ⇔ Kahn's queue never holds 2+ vertices.
  • Process vertices in topological order and you can do DP on a DAG: longest path / critical path in O(V + E) — NP-hard on general graphs, easy on DAGs.
  • Watch the edge direction in the input: LeetCode's [course, pre] means the arrow goes pre → course.

04 · see it

Play with it in 3D

Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.

sim

Kahn's algorithm — in-degree array + queue

Numbers under each vertex are in-degrees. A vertex turns violet when it reaches 0 and joins the queue; lime when output.

loading simulation…
sim

DFS — colours, call stack and the finished stack

Amber = on the current path (gray). A vertex is pushed only once everything it points to is finished. Pop the stack to read the order.

loading simulation…
sim

Cycle detection — spotting the back edge

Course Schedule input `[course, pre]`. When an edge points at an amber (gray) vertex, the red loop is the cycle.

loading simulation…
sim

Lexicographically smallest order — Kahn with a min-heap

Same graph as Kahn above, but the ready set is a PriorityQueue: 0 now beats 2, giving 4, 5, 0, 2, 3, 1.

loading simulation…

05 · cost

Time & space, explained

Topological sort methods (V vertices, E edges)
TimeExtra spacewhy
Kahn (queue)O(V + E)O(V + E)Each vertex is enqueued/polled once, each edge decremented once. Space is the adjacency list plus in-degree array and queue.
DFS + reverse post-orderO(V + E)O(V + E)Each vertex turns gray then black once, each edge is looked at once. Recursion depth can reach V.
Kahn with PriorityQueue (smallest order)O(V log V + E)O(V + E)V offers and V polls on a heap cost O(log V) each; edges are still O(1) each.
Naive: repeatedly scan for a free vertexO(V · (V + E))O(V)Up to V rounds, and each round rescans every vertex and edge to find one with no remaining prerequisite.
Longest path in a DAG (topo order + DP)O(V + E)O(V + E)One topological pass; each edge relaxes its target once. On general graphs the same problem is NP-hard.
Cycle detection in a directed graph
TimeExtra spacewhy
Kahn: output count < VO(V + E)O(V + E)Vertices on a cycle never reach in-degree 0, so they are never output.
DFS with three coloursO(V + E)O(V)Colour array + recursion stack; one back edge to a gray vertex proves a cycle.
DFS with only visited[] (two states)wrong—It can't tell "on my current path" (cycle) from "finished earlier" (fine) — it reports false cycles on diamonds like 0→1, 0→2, 1→3, 2→3.

06 · java

Raw vs Collections Framework

Kahn's algorithm: Collections vs plain arrays

javaint[] queue + int[][] adjacencyjava.util.*
class KahnRaw {    /** Same algorithm with plain arrays only: an int[] works as the queue (head/tail indices). */    static int[] topoSort(int n, int[][] edges) {        int[] outCount = new int[n];        int[] indegree = new int[n];        for (int[] e : edges) {            outCount[e[0]]++;            indegree[e[1]]++;        }        int[][] adj = new int[n][];        for (int v = 0; v < n; v++) adj[v] = new int[outCount[v]];        int[] fill = new int[n];        for (int[] e : edges) adj[e[0]][fill[e[0]]++] = e[1];         int[] queue = new int[n]; // every vertex enters at most once        int head = 0, tail = 0;        for (int v = 0; v < n; v++) if (indegree[v] == 0) queue[tail++] = v;        while (head < tail) {            int u = queue[head++];            for (int v : adj[u]) {                if (--indegree[v] == 0) queue[tail++] = v;            }        }        // The queue array IS the topological order (in the order vertices were enqueued).        return tail == n ? queue : new int[0];    }}
  • Each vertex enters the queue at most once, so a plain int[n] with head/tail indices is a complete queue — and it is the output order.
  • The Collections version is easier to write under pressure. ArrayDeque beats LinkedList as a queue (no node objects) and Stack (synchronized, legacy).
  • Building int[][] adjacency needs two passes (count, then fill) — faster in hot loops, but more code. Use it when E is in the millions.
  • Return an empty array (or false) when output size < V — that is the cycle check.

DFS: three colours and a finished stack

javareverse post-orderjava.util.*
class DfsTopo {    static final int WHITE = 0, GRAY = 1, BLACK = 2;     /** Reverse post-order DFS. Returns [] if a cycle (back edge to a GRAY vertex) exists. */    static int[] topoSort(int n, int[][] edges) {        List<List<Integer>> adj = new ArrayList<>();        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());        for (int[] e : edges) adj.get(e[0]).add(e[1]);        int[] color = new int[n];        Deque<Integer> finished = new ArrayDeque<>(); // used as a stack        for (int v = 0; v < n; v++) {            if (color[v] == WHITE && !dfs(v, adj, color, finished)) return new int[0];        }        int[] order = new int[n];        for (int i = 0; i < n; i++) order[i] = finished.pop();        return order;    }     private static boolean dfs(int u, List<List<Integer>> adj, int[] color, Deque<Integer> finished) {        color[u] = GRAY;        for (int v : adj.get(u)) {            if (color[v] == GRAY) return false;            if (color[v] == WHITE && !dfs(v, adj, color, finished)) return false;        }        color[u] = BLACK;        finished.push(u);        return true;    }}
  • finished.push(u) happens after all of u's neighbours are done. Popping the stack therefore lists u before everything it points to.
  • Using ArrayDeque as a stack: push/pop work on the head. Don't use java.util.Stack — it's a synchronized Vector.
  • Java's default thread stack overflows around 10⁴–10⁵ nested calls. For big inputs prefer Kahn (iterative) or run DFS with an explicit stack.

Smallest order: PriorityQueue instead of a queue

javaKahn + min-heapjava.util.*
class SmallestOrder {    /** Kahn's algorithm with a min-heap: always take the smallest ready vertex. */    static int[] topoSort(int n, int[][] edges) {        List<List<Integer>> adj = new ArrayList<>();        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());        int[] indegree = new int[n];        for (int[] e : edges) {            adj.get(e[0]).add(e[1]);            indegree[e[1]]++;        }        PriorityQueue<Integer> ready = new PriorityQueue<>(); // min-heap        for (int v = 0; v < n; v++) {            if (indegree[v] == 0) ready.offer(v);        }        int[] order = new int[n];        int idx = 0;        while (!ready.isEmpty()) {            int u = ready.poll();            order[idx++] = u;            for (int v : adj.get(u)) {                indegree[v]--;                if (indegree[v] == 0) ready.offer(v);            }        }        return idx == n ? order : new int[0];    }}
  • new PriorityQueue<>() on Integer is a min-heap by natural order. For strings or custom ranking pass a Comparator.
  • Only Kahn can do this directly: DFS has no natural point to "pick the smallest ready vertex".

DP on a DAG: longest (critical) path

javarelax edges in topological orderjava.util.*
class LongestPath {    /** Weighted DAG: edges[i] = {from, to, weight}. Longest path length starting anywhere. */    static long longestPath(int n, int[][] edges) {        List<List<int[]>> adj = new ArrayList<>();        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());        int[] indegree = new int[n];        for (int[] e : edges) {            adj.get(e[0]).add(new int[] {e[1], e[2]});            indegree[e[1]]++;        }        long[] best = new long[n]; // best[v] = longest path that ENDS at v        Deque<Integer> queue = new ArrayDeque<>();        for (int v = 0; v < n; v++) if (indegree[v] == 0) queue.offer(v);        long answer = 0;        while (!queue.isEmpty()) {            int u = queue.poll(); // all of u's predecessors are final now            answer = Math.max(answer, best[u]);            for (int[] edge : adj.get(u)) {                int v = edge[0];                best[v] = Math.max(best[v], best[u] + edge[1]); // relax "upwards"                if (--indegree[v] == 0) queue.offer(v);            }        }        return answer;    }}
  • When a vertex is polled, all its predecessors are final — so best[u] is final too. That's why DP in topological order is correct.
  • Use long when weights can add up beyond ~2·10⁹.
  • The same loop with min gives shortest paths in a DAG — even with negative weights, in O(V + E).
Java API you will actually use
calldoescost
new ArrayDeque<Integer>()Queue for Kahn (offer/poll) or stack for DFS (push/pop)O(1) amortized per op
new PriorityQueue<Integer>()Min-heap of ready vertices → smallest orderO(log n) offer/poll
List<List<Integer>> adj = new ArrayList<>()Adjacency list; add n empty lists firstO(1) amortized add
map.computeIfAbsent(k, x -> new ArrayList<>()).add(v)Adjacency when vertices are strings/charsO(1) average
map.merge(k, 1, Integer::sum)Increment an in-degree stored in a mapO(1) average
int[] indegree = new int[n]In-degree counters, all start at 0O(n)
Arrays.fill(color, 0)Reset colours between test casesO(n)

Any valid order, or "is it possible?"

→ Kahn with ArrayDeque

Iterative (no stack overflow), cycle check is just a counter.

Lexicographically smallest order

→ Kahn with PriorityQueue

Always pick the smallest ready vertex: O(V log V + E).

Which vertices are on / lead into a cycle

→ DFS colours, or Kahn on the reversed graph

Gray-at-the-end vertices are exactly the unsafe ones (LC 802).

Longest path, earliest finish time, max chain

→ Kahn + DP (or DFS + memo)

Topological order guarantees predecessors are final before you use them.

Peel a tree from the outside in (centres)

→ Kahn on leaves (degree 1)

Same idea, undirected: repeatedly remove the vertices with nothing "behind" them.

07 · recognise

Spot the pattern

"prerequisites", "depends on", "must happen before", "build order"

Directed graph + topological sort

"Is it possible to finish / schedule everything?"

Cycle detection: Kahn count or DFS gray back edge

Derive an ordering from sorted examples (alien dictionary)

Compare adjacent items, first difference = an edge, then topo sort

"Minimum time when tasks run in parallel"

Longest path in a DAG (Kahn + DP)

"Smallest / lexicographically first valid order"

Kahn with a PriorityQueue

Strictly increasing moves on a grid (longest increasing path)

Implicit DAG: memoised DFS or peel by in-degree

"Find the centre / root that minimises height" of a tree

Peel leaves layer by layer (Kahn on degree 1)

08 · traps & tricks

Mistakes to avoid, tricks to use

traps

  • Reversing edge direction: [course, pre] means pre → course. Draw one edge on paper before coding.
  • Using a two-state visited[] for directed cycle detection — a diamond (two paths into one vertex) looks like a cycle. You need gray vs black.
  • Forgetting to reverse the DFS post-order (or reversing a Kahn order, which is already correct).
  • Forgetting vertices with no edges at all — loop over all V vertices when seeding / starting DFS.
  • Counting duplicate edges twice in in-degree but adding them once to adjacency (or vice versa) — keep both consistent (Alien Dictionary uses a Set).
  • Deep recursion on 10⁵ vertices → StackOverflowError in Java. Switch to Kahn.
  • Returning a partial order when a cycle exists. Check count == V before returning.

tricks

  • Default to Kahn in interviews: iterative, cycle check is one line, and it extends to PriorityQueue and DP.
  • Say the invariant: "a vertex enters the queue exactly when all its prerequisites are already output."
  • Dry-run on a 4-vertex diamond (0→1, 0→2, 1→3, 2→3) — it catches both direction bugs and visited-vs-colour bugs. Bas itna yaad rakho: draw it first, code second.
  • Mention both methods and the trade-off (BFS vs recursion depth) — interviewers love hearing you know two ways.
  • If the answer must be "any valid order", say so — and validate with a check that every edge points forward.

09 · why & how

Questions you might be too shy to ask

What exactly is in-degree, and why does 0 mean "ready"?

In-degree = how many arrows point into a vertex = how many things it is still waiting for. For prerequisites [[1,0],[2,0],[2,1]] (arrows 0→1, 0→2, 1→2): course 0 has in-degree 0, course 1 has 1, course 2 has 2. In-degree 0 means nothing is pending, so it can go next. Each time a vertex is output, its neighbours lose one pending prerequisite: indegree[v]--.

Isn't Kahn just BFS? Can I use a normal BFS order from the start vertex?

No — plain BFS orders vertices by distance from the start, not by dependencies. Edges 0→1, 0→2, 2→1: if 0's list is [1, 2], BFS from 0 visits 1 before 2 (both are one hop away), but 2 → 1 says 2 must come first. Kahn looks like BFS, but a vertex joins the queue only when its in-degree hits 0 — when all its prerequisites are done, not when it is first seen.

Why does a cycle make a topological order impossible?

Take a cycle a → b → c → a. In any list, one of these three appears first, say a. But c → a says c must come before a. Contradiction — so no order can satisfy all edges.

Why does reversing the DFS finish order work?

When u finishes, every vertex u points to has already finished — it was either black before we got there or we finished it during u's own DFS (it can't be gray, or that would be a cycle). So for every edge u → v, v is pushed before u. Reversing puts u before v — exactly what we need.

Kahn or DFS — which should I write?

Kahn by default: it's iterative (no stack overflow), the cycle check is a counter, and it upgrades easily to PriorityQueue (smallest order) and DP (longest path). Use DFS when you already have a DFS skeleton or need per-vertex results like "safe" states.

Why three colours? Isn't visited[] enough?

In a directed graph, reaching a vertex you finished earlier is fine (e.g. two courses share a prerequisite). Only reaching a vertex that is still on the current path (gray) means you went in a circle. Two states can't tell those apart.

What if the graph is disconnected, or some vertex has no edges at all?

Still fine — the order just mixes the pieces in some way. Kahn: seed the queue with every in-degree-0 vertex, not only vertex 0; a lonely vertex has in-degree 0 and comes out right away. DFS: start a new DFS from every vertex that is still white. Miss this and your output is shorter than V, which looks like a fake cycle.

Can I print the actual cycle, not just say "there is one"?

Yes, with DFS and a parent[] array. When an edge u → v hits a gray v, the cycle is v → … → u → v: walk parent back from u until you reach v. Kahn can't do this cleanly — its leftover vertices are the ones on a cycle plus everything that depends on one (can be reached from it).

How many valid orders can there be?

Possibly a huge number — a graph with no edges has V! orders. Counting them exactly is a known hard problem in general (#P-complete, proved by Brightwell and Winkler in 1991). The order is unique exactly when Kahn's queue never holds more than one vertex at a time.

Does the order from my code have to match LeetCode's example exactly?

No — problems like Course Schedule II accept any valid order. The judge checks that every prerequisite comes first. Only problems that ask for a specific order (e.g. smallest) need a PriorityQueue.

Alien Dictionary: why only the first differing letter, and only neighbouring words?

Think of a normal dictionary: "cat" comes before "dog" only because c < d. The letters after that (a vs o) were never compared, so they prove nothing. Neighbours are enough because sorted order chains: if w1 ≤ w2 and w2 ≤ w3, then w1 ≤ w3. So any letter order that keeps every neighbouring pair in order keeps the whole list sorted. Comparing far-apart pairs only repeats what the neighbour rules already imply.

Find Eventual Safe States: why reverse the edges before running Kahn?

Safety depends on where you can go: a node is safe when every outgoing edge leads to a safe node. Kahn frees a vertex when its incoming count hits 0. Flip every edge and "all my exits are safe" becomes "all my incoming edges are done" — terminal nodes become the starting sources. The counter is simply outdegree[u] from the original graph.

Does topological sort work on undirected graphs?

Not as such — an undirected edge has no "before". But the same peeling idea works on trees: Minimum Height Trees removes degree-1 leaves layer by layer, exactly like Kahn removes in-degree-0 vertices.

Minimum Height Trees: why peel a whole layer of leaves at once, not one leaf at a time?

Both ends of the longest path must shrink at the same speed. Path 0-1-2-3-4, one leaf at a time: remove 0, then 4, then 1 — now only 2 and 3 remain, so you stop and return [2, 3]. The right answer is [2]. Layer by layer: remove {0, 4}, then {1, 3}, and only 2 is left. Dhyan do: count the current layer's size before you start removing it.

10 · practice

Interview problems

Every problem has brute → better → optimal solutions in Java, with complexity and a dry run.

  1. #207
    Course Schedulemust-do
    MediumByteDance / TikTokAmazonAppleMetaUber+1
  2. #210
    Course Schedule IImust-do
    MediumNetflixByteDance / TikTokAppleAmazonUber+1
  3. #953
  4. #269
    Alien Dictionarymust-doLeetCode Premium
    HardUberAirbnbMetaAmazonGoogle
  5. #802
    Find Eventual Safe States
    MediumAmazonGoogle
  6. #2050
    Parallel Courses III
    HardGoogleGoldman SachsAmazon
  7. #310
    Minimum Height Treesmust-do
    MediumGoogleAmazonMicrosoft
  8. #329
    Longest Increasing Path in a Matrixmust-do
    HardByteDance / TikTokMetaGoogleMicrosoftAmazon+1
  9. #2392

11 · revise

The whole chapter on one screen

9 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.

  1. what it is

    A topological order lists every vertex so all arrows point forward. It exists exactly when the directed graph has no cycle (a DAG).

  2. use it when

    "Prerequisites", "depends on", "build order", "is it possible to finish?" — or an order hidden in sorted examples (alien dictionary).

  3. template

    Kahn: count in-degrees → queue every 0 → poll, output, decrement neighbours, queue new 0s. Output count < V ⇒ cycle.

  4. cost

    Kahn and DFS: O(V + E) time and space. Smallest order with a PriorityQueue: O(V log V + E). Longest path on a DAG: O(V + E).

  5. trap

    [course, pre] means the arrow goes pre → course. Draw one edge on paper before you code.

  6. trap

    Directed cycle check needs three colours. A plain visited[] calls a harmless diamond a cycle.

  7. trap

    DFS gives the finish order reversed — pop the stack. Kahn's order is already correct; don't reverse it.

  8. pro tip

    Default to Kahn in Java: no recursion depth limit, a one-line cycle check, and it upgrades to min-heap order and DP.

  9. pro tip

    Process vertices in topological order and DP gets easy: critical path, earliest finish, counting paths.

12 · recall

Flip, remember, repeat

card 1 / 13
recall

When does a topological order exist?

tap or Space to flip ↻

Question 1: When does a topological order exist?

13 · check yourself

Quick quiz

  1. Kahn's algorithm finished and output 5 of 7 vertices. What does that mean?

  2. During directed DFS you reach a vertex that is BLACK. What do you do?

  3. Edges: 0→1, 0→2, 1→3, 2→3. Which is NOT a valid topological order?

  4. You need the lexicographically smallest topological order. What changes in Kahn?

  5. Course Schedule gives prerequisites [a, b]. Which edge do you add?

  6. Why does memoised DFS in Longest Increasing Path need no visited[] array?

  7. What is the time complexity of Kahn's algorithm?