Strongly Connected Components

Consider a network of web pages linked by hyperlinks, or a set of towns connected by one-way roads. Some groups of pages link to each other so thoroughly that you can browse from any page in the group back to any other. Other pages sit in one-way chains that you can never return from. Detecting these mutually-reachable clusters is the problem of finding strongly connected components (SCCs). It is one of the cleanest applications of depth-first search to a genuinely structural question about directed graphs. SCCs matter because directed graphs are rarely as tidy as trees or DAGs. Compiler dependency graphs, deadlock-detection graphs in operating systems, citation networks, and the call graphs of recursive functions all contain cycles. DFS alone does not resolve these cycles into a hierarchy. Collapsing each strongly connected component into a single supernode always produces a directed acyclic graph. This is called the component graph, and it can then be topologically sorted, scheduled, or analyzed layer by layer. This collapsing step turns a messy cyclic graph into a clean DAG. It is the same DAG structure that underlies build systems, spreadsheet recalculation, and course-prerequisite planning. This article presents Kosaraju's algorithm, a beautifully economical method. It computes all SCCs using exactly two depth-first searches. One search runs on the graph GG, the other on its transpose GTG^T (the graph with every edge reversed). The two-pass structure is not a coincidence. It is forced by a precise theorem about how DFS finish times interact with the component graph. Understanding that theorem is the real payoff of studying this algorithm. By the end of this article you will be able to run Kosaraju's algorithm by hand. You will prove that it is correct using the component-DAG argument. You will analyze its O(V+E)O(V+E) running time. You will also recognize the classic implementation traps. This especially includes the finish-time ordering mistake that silently produces wrong answers on graphs that look correct at first glance.

Key takeaways
  • A strongly connected component (SCC) is a maximal set of vertices where every vertex can reach every other vertex.
  • Collapsing each SCC into one node always yields a DAG: the component graph GSCCG^{SCC}.
  • Kosaraju's algorithm: DFS on GG to get finish times, then DFS on GTG^T processing vertices in decreasing finish-time order.
  • Each DFS tree produced in the second pass is exactly one SCC — this is the theorem to prove, not just believe.
  • Running time is O(V+E)O(V + E): two linear-time DFS passes plus one linear-time transpose construction.
  • The classic bug is running the second DFS in the wrong order (increasing instead of decreasing finish time), which silently breaks correctness.
The core insight

Running DFS on GG to rank vertices by finish time, then DFS on GTG^T in that ranked order, forces each resulting DFS tree to align exactly with one strongly connected component.

The idea

A directed graph G=(V,E)G = (V, E) has vertices uu and vv that are strongly connected if there is a path from uu to vv and a path from vv to uu. Strong connectivity is an equivalence relation on VV: it is reflexive (a vertex trivially reaches itself), symmetric by definition, and transitive (if u↔vu \leftrightarrow v and v↔wv \leftrightarrow w then u↔wu \leftrightarrow w by concatenating paths). Because it is an equivalence relation, it partitions VV into disjoint equivalence classes — these classes are the strongly connected components of GG. Each SCC is a maximal subset of vertices that are pairwise mutually reachable. Given the SCCs C1,…,CkC_1, \dots, C_k, define the component graph GSCCG^{SCC} by contracting each CiC_i to a single supernode, and drawing an edge Ci→CjC_i \to C_j whenever some edge in GG goes from a vertex in CiC_i to a vertex in CjC_j (for i≠ji \ne j). A short argument shows GSCCG^{SCC} is always acyclic: if GSCCG^{SCC} had a cycle through supernodes Ci1→Ci2→⋯→Ci1C_{i_1} \to C_{i_2} \to \dots \to C_{i_1}, then every vertex in every CijC_{i_j} could reach every vertex in every other CijC_{i_j} through this cycle, forcing them all into one SCC — contradicting that the CijC_{i_j} were distinct maximal components. This acyclicity is exactly why Topological Sort applies to the component graph once it is built, and why SCC decomposition is often the first preprocessing step before running DAG-based scheduling algorithms. Kosaraju's algorithm exploits DFS Edge Classification and Timestamps with a clever twist: it runs DFS twice, once forward on GG and once backward on the transpose graph GTG^T (same vertex set, every edge reversed). Note that GG and GTG^T have exactly the same SCCs, since reversing every edge reverses every path, so mutual reachability is preserved. The key insight — proved in the next section — is that if you process vertices in the second DFS in decreasing order of their finish times from the first DFS, each resulting DFS tree in the forest is precisely one SCC.

How it works

The algorithm has three phases: a DFS on GG recording finish times, a transposition of the graph, and a DFS on GTG^T visiting vertices in decreasing finish-time order.

Phase 1. Run DFS on GG. Every time a vertex's adjacency list is exhausted (its DFS call is about to return), stamp it with a finish time and push it onto a stack (or append to a list). The vertex finishing last ends up on top.

Phase 2. Build GTG^T by reversing every edge of GG. If GG is given as adjacency lists, this takes O(V+E)O(V+E) time: scan every edge (u,v)(u,v) once and insert (v,u)(v,u) into GTG^T's adjacency list.

Phase 3. Run DFS on GTG^T, but choose the next unvisited root strictly in decreasing order of the Phase 1 finish times (equivalently, pop vertices off the stack built in Phase 1). Each tree produced by this DFS forest is output as one strongly connected component. ``` KOSARAJU-SCC(G): // Phase 1: DFS on G, record finish order for each vertex u in G.V: u.visited = false finishOrder = empty stack for each vertex u in G.V: if not u.visited: DFS-VISIT-1(G, u, finishOrder) // Phase 2: build transpose G_T = TRANSPOSE(G) // Phase 3: DFS on G_T, in decreasing finish order for each vertex u in G_T.V: u.visited = false components = empty list while finishOrder is not empty: u = finishOrder.pop() if not u.visited: newComponent = empty set DFS-VISIT-2(G_T, u, newComponent) components.append(newComponent) return components

DFS-VISIT-1(G, u, finishOrder): u.visited = true for each v in G.adj[u]: if not v.visited: DFS-VISIT-1(G, v, finishOrder) finishOrder.push(u) // stamp finish time by push order

DFS-VISIT-2(G_T, u, component): u.visited = true component.add(u) for each v in G_T.adj[u]: if not v.visited: DFS-VISIT-2(G_T, v, component)

TRANSPOSE(G): G_T = new graph with same vertices as G, no edges for each vertex u in G.V: for each v in G.adj[u]: G_T.adj[v].add(u) return G_T ``` Every vertex is visited exactly once in each DFS pass, and every edge is examined exactly once per pass (once in GG, once in GTG^T), so both DFS phases and the transpose construction are linear in the size of the graph.

Figure
Kosaraju's algorithm on a 7-vertex example: the DFS finish stack from G, and the resulting SCC forest from G-transpose
Graph G A B C D E F G (D→G edge cont'd) SCC 1 = {A,B,C} (blue) SCC 2 = {D,E,F} (orange) SCC 3 = {G} (green) Finish stack from DFS(G) top: G (finished last... ) E, F, D (some order) B, C, A (some order) Pop order for Phase 3: G → D-or-E-or-F → ... Component graph G^SCC (acyclic) {A,B,C} {D,E,F} {G} edge C→D induces this edge D→G induces this

The diagram shows the essential shape: GG has three SCCs (the blue triangle {A,B,C}\{A,B,C\}, the orange triangle {D,E,F}\{D,E,F\}, and the singleton {G}\{G\}), the component graph collapsing them is a simple chain (hence acyclic), and the finish stack from Phase 1 always places a vertex of the "last" SCC in the topological order ({A,B,C}\{A,B,C\}, the source of the DAG) on top, guaranteeing it is popped first in Phase 3.

Complexity & correctness

Running time. Let n=∣V∣n = |V| and m=∣E∣m = |E|. Phase 1 is a standard DFS: O(n+m)O(n+m). Building GTG^T scans every edge once: O(n+m)O(n+m). Phase 3 is another standard DFS over GTG^T, which has the same number of vertices and edges as GG: O(n+m)O(n+m). Summing three linear phases gives total time

T(n,m)=O(n+m),T(n,m) = O(n+m),

which is asymptotically optimal — any correct algorithm must at least read the input. Space is O(n+m)O(n+m) for the adjacency lists of GG and GTG^T, plus O(n)O(n) for the finish-time stack, visited flags, and recursion stack.

Correctness — the component-DAG argument. The proof rests on one key lemma about DFS finish times applied to the component graph. Lemma (finish-time ordering across components). Let CC and C′C' be distinct SCCs of GG, and suppose there is an edge from some vertex of CC to some vertex of C′C' in GSCCG^{SCC} (i.e., C→C′C \to C'). Let f(C)f(C) denote the maximum finish time, over a DFS of GG, of any vertex in CC. Then

f(C)>f(C′).f(C) > f(C').

Proof sketch. Consider whichever of CC or C′C' is discovered first by the DFS, and let xx be the first vertex discovered among C∪C′C \cup C'.

  • Case 1: x∈Cx \in C. Since C→C′C \to C' and (because GSCCG^{SCC} is acyclic) there is no edge back from C′C' to CC, every vertex of C′C' reachable from xx is reached via some path leaving CC that never returns. At the moment xx is discovered, all of CC and all of C′C' reachable from CC are still white, so by the white-path property of DFS, xx's DFS subtree will finish after completely exploring both CC and C′C' (since C′C' is reachable from CC and hence from xx before xx finishes). Hence xx, and therefore some vertex of CC, finishes after every vertex of C′C': f(C)>f(C′)f(C) > f(C').
  • Case 2: x∈C′x \in C'. Then since there is no edge from C′C' back into CC (acyclicity of GSCCG^{SCC} again), DFS from xx cannot reach any vertex of CC. So the entire component C′C' is discovered and finished — including the vertex achieving f(C′)f(C') — strictly before DFS ever discovers any vertex of CC (because xx was the first vertex found in C∪C′C \cup C', so CC vertices are still white when C′C' finishes). Hence every vertex of CC finishes later, giving f(C)>f(C′)f(C) > f(C') again.

In both cases f(C)>f(C′)f(C) > f(C'), proving the lemma. ■\blacksquare Consequence. This lemma says that if we rank the SCCs by their maximum finish time f(C)f(C) from a DFS of GG, that ranking is exactly a reverse topological order of GSCCG^{SCC} — every edge C→C′C \to C' has f(C)>f(C′)f(C) > f(C'), so the component with globally largest finish time is a source of GSCCG^{SCC} (nothing points into it from another component, since anything pointing in would have to finish even later). Why the second pass then works. In Phase 3 we run DFS on GTG^T, and GSCCTG^{SCC^T} (the component graph of the transpose) is exactly GSCCG^{SCC} with every edge reversed. So the component with the largest f(C)f(C)-value — a source in GSCCG^{SCC} — becomes a sink in GSCCTG^{SCC^T}: no edges leave it in the transposed component graph. We start Phase 3 at a vertex of that component (it sits on top of the finish stack). Since it is a sink in GSCCTG^{SCC^T}, DFS from it in GTG^T cannot escape to any other component — the search explores exactly the vertices of that one SCC (they are mutually reachable within the component, and reachability confirms it can't leak out). So the first DFS tree grown in Phase 3 is exactly that one SCC. Now remove that finished component and repeat the argument inductively: among the remaining unvisited components, whichever has the next-highest f(C)f(C) value is a source in the remaining subgraph of GSCCG^{SCC}, hence a sink in the remaining subgraph of GSCCTG^{SCC^T}, and it sits highest on the still-unpopped part of the stack. So Phase 3's next DFS tree, rooted there, again cannot escape its own component. By induction, every DFS tree produced in Phase 3 is exactly one SCC, and every vertex is assigned to exactly one tree (since Phase 3 is a complete DFS forest over all vertices of GTG^T). This proves Kosaraju's algorithm partitions VV correctly into SCCs. ■\blacksquare

Tradeoffs. Kosaraju's algorithm is conceptually the simplest linear-time SCC algorithm to prove correct, because the proof reduces to one clean lemma about finish times and component-DAG structure. Its practical downside is needing to materialize GTG^T explicitly and store two full adjacency structures, and needing two full DFS passes with an intervening full graph traversal to sort by finish time. Tarjan's single-pass algorithm and the path-based Gabow algorithm achieve the same O(V+E)O(V+E) bound with one DFS pass and no explicit transpose, at the cost of a more intricate low-link invariant that is harder to prove correct from first principles — Kosaraju's algorithm is usually taught first precisely because its proof is so self-contained.

Worked example

Take the graph from the diagram above with vertices {A,B,C,D,E,F,G}\{A,B,C,D,E,F,G\} and edges:

A ⁣→ ⁣B,  B ⁣→ ⁣C,  C ⁣→ ⁣A,  C ⁣→ ⁣D,  D ⁣→ ⁣E,  E ⁣→ ⁣F,  F ⁣→ ⁣D,  D ⁣→ ⁣G.A\!\to\!B,\; B\!\to\!C,\; C\!\to\!A,\; C\!\to\!D,\; D\!\to\!E,\; E\!\to\!F,\; F\!\to\!D,\; D\!\to\!G.

Phase 1 — DFS on GG, starting at AA (assume adjacency lists list neighbors alphabetically):

StepActionStack pushed (finish order)
1Visit AA, go to BB
2Visit BB, go to CC
3Visit CC, neighbor AA visited, go to DD
4Visit DD, go to EE
5Visit EE, go to FF
6Visit FF, neighbor DD visited (no white neighbors) → finish FFpush FF
7Back to EE, no more neighbors → finish EEpush EE
8Back to DD, next neighbor GG, visit GG
9GG has no outgoing edges → finish GGpush GG
10Back to DD, no more neighbors → finish DDpush DD
11Back to CC, no more neighbors → finish CCpush CC
12Back to BB, no more neighbors → finish BBpush BB
13Back to AA, no more neighbors → finish AApush AA

Stack from bottom to top: F,E,G,D,C,B,AF, E, G, D, C, B, A. Popping gives the order A,B,C,D,G,E,FA, B, C, D, G, E, F.

Phase 2 — Transpose. Reversing every edge gives GTG^T: B ⁣→ ⁣A, C ⁣→ ⁣B, A ⁣→ ⁣C, D ⁣→ ⁣C, E ⁣→ ⁣D, F ⁣→ ⁣E, D ⁣→ ⁣F, G ⁣→ ⁣DB\!\to\!A,\ C\!\to\!B,\ A\!\to\!C,\ D\!\to\!C,\ E\!\to\!D,\ F\!\to\!E,\ D\!\to\!F,\ G\!\to\!D.

Phase 3 — DFS on GTG^T in the order A,B,C,D,G,E,FA, B, C, D, G, E, F (popped from the stack):

PopVisited already?DFS-tree grownResulting SCC
AAnoA→C→BA \to C \to B (B→AB\to A visited, back to CC: C→BC\to B, BB's only edge B→AB\to A visited){A,B,C}\{A, B, C\}
BByes——
CCyes——
DDnoD→CD \to C (visited, skip) →F→E\to F \to E (E→DE\to D visited); no escape to {A,B,C}\{A,B,C\} or to GG{D,E,F}\{D, E, F\}
GGnoG→DG \to D (already visited, skip){G}\{G\}
EEyes——
FFyes——

Output: three SCCs, {A,B,C}\{A,B,C\}, {D,E,F}\{D,E,F\}, {G}\{G\} — matching the diagram exactly, and matching the ground truth since A,B,CA,B,C form a 3-cycle, D,E,FD,E,F form a 3-cycle, and GG has no outgoing or return path. Notice the crucial detail: even though D→GD \to G is an edge in GG, when Phase 3 reaches the popped vertex DD and explores GTG^T, the edge in GTG^T is G→DG \to D, not D→GD \to G — so DFS from DD never reaches GG, correctly keeping GG isolated as its own SCC, and confirming the component graph really is the acyclic chain {A,B,C}→{D,E,F}→{G}\{A,B,C\} \to \{D,E,F\} \to \{G\}.

Common pitfalls & traps

PitfallWhy it happensFix
Running Phase 3 in increasing finish-time orderConfusing "process by finish time" with "smallest first," as in some greedy algorithmsAlways pop from the top of the finish stack (largest finish time first) — verify against the proof's reverse-topological argument
Forgetting to reset `visited` flags between Phase 1 and Phase 3Reusing the same visited array for both DFS passesUse a fresh visited array (or reset it) before the second DFS
Building GTG^T incorrectly (e.g. reversing only some edges or leaving self-loops out)Off-by-one style bugs when writing the transpose loop, or overlooking multi-edgesIterate every adjacency list of GG exactly once, insert the reverse edge, and mirror any self-loops as-is
Treating a single DFS on GG alone as sufficientBelieving finish order alone identifies SCCs without the transpose passRemember: finish order gives a valid ordering of the component DAG, not the components themselves — the transpose pass is what isolates each component
Off-by-one in stack indexing when implementing iterativelyManual stack management for recursion-free DFS can mishandle push/pop timing relative to "finish"Only mark a vertex "finished" (and push it) after all its neighbors have been fully explored, not when it is first discovered
Assuming SCCs are contiguous or singletons in sparse-looking graphsVisual intuition from small examplesAlways verify SCCs are maximal via the mutual-reachability definition, not visual grouping

Pioneers & history

Kosaraju's algorithm is named for S. Rao Kosaraju, who described the two-pass DFS-on-transpose technique in an unpublished 1978 note; the same idea was independently rediscovered and popularized by Micha Sharir in 1981, and it is sometimes called the Kosaraju–Sharir algorithm. The technique sits alongside Robert Tarjan's 1972 single-pass low-link algorithm as one of the two classic linear-time methods for SCC decomposition, both building directly on the DFS discovery/finish-time framework that Tarjan himself had systematized for reasoning about graph structure.

Origins · 1 minRobert Endre Tarjan and how this was discovered
RE
Robert Endre Tarjan
born 1948 · Pioneer of linear-time graph algorithms and amortized analysis

Tarjan developed the DFS-based low-link technique for strongly connected components and biconnected components in the early 1970s, and later co-invented the union-find analysis, splay trees, and Fibonacci heaps, establishing much of the toolkit modern graph algorithms still rely on.

History
1972
— Tarjan publishes his single-pass DFS algorithm for strongly connected components.
1978
— Kosaraju describes the two-pass DFS-on-transpose method in an unpublished manuscript.
1981
— Sharir independently publishes and popularizes the same two-pass approach.
1983
— Aho, Hopcroft, and Ullman's textbook treatment helps standardize DFS-based SCC algorithms in computer science curricula.
2000
s — SCC decomposition becomes a routine preprocessing step in large-scale web-graph and dependency-graph analysis pipelines.

Where this matters

Compilers and build systems use SCC decomposition to find groups of mutually recursive functions or circular module dependencies before doing dependency-ordered compilation; each SCC in the call graph must be compiled or type-checked as a unit, and the acyclic component graph then gives a valid compilation order via Topological Sort. Package managers and dependency resolvers (in language ecosystems and container-build tools) use the same idea to detect cyclic dependencies that must be flagged to the user rather than silently ordered. Deadlock detection in operating systems and database systems models resource-allocation graphs as directed graphs, where a cycle among "waiting-for" edges signals a deadlock; SCC computation finds all such cycles simultaneously rather than searching for one cycle at a time. Web-scale graph analysis (search-engine crawling, citation-network research, social-network community structure) uses SCC decomposition to identify the "core" of mutually-linking pages versus pages that are only ever linked to or only ever link out, a structure famously described in large-scale studies of the web graph's "bowtie" shape. Model checking and static analysis tools that build control-flow or state-transition graphs use SCCs to isolate loops and recursive structures for separate fixpoint or termination analysis.

Ask the tutor: How does Tarjan's single-pass low-link algorithm find the same SCCs without ever building the transpose graph?

Why does Kosaraju's algorithm need two separate DFS passes instead of one?

A single DFS on GG produces a valid topological-style ordering of the component graph GSCCG^{SCC} (via the finish-time lemma), but it does not by itself separate vertices into components — a single DFS tree can span multiple SCCs whenever one component reaches another. The second pass, on the transpose graph and in the finish-time order discovered by the first pass, exploits the fact that reversing edges turns each "source" component in the first pass's ordering into a "sink" component with no outgoing edges to other unvisited components. That sink property is what prevents the second DFS from leaking outside a single SCC, so the two passes together — not either alone — are what isolate the components.

Is Kosaraju's algorithm the fastest way to find strongly connected components?

Asymptotically, no algorithm beats O(V+E)O(V+E) for SCCs since that is linear in the input size, and Kosaraju's algorithm already achieves this bound — so it is asymptotically optimal, tied with Tarjan's algorithm and the Gabow path-based algorithm. In practice, Tarjan's single-pass algorithm is often preferred because it avoids explicitly constructing the transpose graph and needs only one DFS traversal with a small amount of extra per-vertex bookkeeping (a low-link value). Kosaraju's algorithm is usually preferred pedagogically because its correctness proof is shorter and more modular, resting on one clean lemma about finish times.

Does Kosaraju's algorithm work on undirected graphs?

It is designed for directed graphs; on an undirected graph, "strongly connected" and "connected" coincide, since every edge is trivially traversable both ways, and a single DFS or BFS pass already finds ordinary connected components in O(V+E)O(V+E) time. Running Kosaraju's two-pass machinery on an undirected graph still gives the correct answer (each connected component becomes one SCC) but is wasteful, since the transpose of an undirected graph (as a symmetric directed graph) is identical to the graph itself, making the second pass redundant.

What happens if the graph has self-loops or multiple edges between the same pair of vertices?

Self-loops do not affect SCC membership: a vertex with a self-loop is still just one vertex, trivially strongly connected to itself regardless of the loop, so DFS treats it the same as a vertex without one (the loop edge is simply skipped since the vertex is already visited). Multiple parallel edges between the same ordered pair of vertices also do not change the SCC structure, since Kosaraju's algorithm only cares about reachability, not edge multiplicity; a good implementation should still be careful when building the transpose to preserve (or safely deduplicate) parallel edges depending on whether edge weights or counts matter downstream.

Can Kosaraju's algorithm be implemented iteratively instead of recursively?

Yes, and for large graphs this is often necessary to avoid recursion-depth limits (a long directed path can cause O(V)O(V) recursion depth). Both DFS passes can be rewritten using an explicit stack that simulates the call stack, being careful to only mark a vertex as "finished" (and push it to the finish-order stack in Phase 1) after all of its neighbors have been processed, not merely upon first visiting it. This iterative rewrite does not change the asymptotic complexity, since it still visits every vertex and edge exactly once per pass.

How is the component graph G^SCC actually constructed once SCCs are known?

After Phase 3 assigns every vertex to a component (e.g., by giving it a component ID equal to the index of the DFS tree that contains it), scan every original edge (u,v)(u,v) of GG once: if uu and vv have different component IDs, add an edge between those two component IDs in GSCCG^{SCC} (deduplicating parallel edges if desired). This scan takes O(E)O(E) time on top of the O(V+E)O(V+E) already spent finding the components, so building the full component DAG remains linear overall, ready for Topological Sort or further DAG-based processing.

What role do DFS discovery and finish timestamps play beyond this algorithm?

Discovery and finish timestamps, as introduced when studying DFS Edge Classification and Timestamps, are the same bookkeeping device used to classify edges as tree, back, forward, or cross edges, and to detect cycles (a back edge implies a cycle in a directed graph). Kosaraju's algorithm is a striking demonstration that these timestamps carry global structural information — specifically, a valid reverse-topological ranking of the component graph — not just local information about a single DFS tree, which is why the same timestamp machinery reappears across many graph algorithms.

Before this