🎁 立即注册,最多可免费使用 30 分钟在线 AI,无需信用卡。

Kosaraju 算法:强连通分量指南

August 22, 2026
Kosaraju 算法如何通过两次 DFS 遍历找到强连通分量,包含实例演示、经过测试的 Python 代码、复杂度和常见错误。
Kosaraju 算法:强连通分量指南
Kosaraju 算法
强连通分量
图算法
编程面试准备

Kosaraju 算法用于找出有向图的强连通分量(SCC)。所谓 SCC,就是一组顶点,其中任意一个顶点都能到达其他所有顶点。该算法执行两次深度优先搜索:第一次记录顶点的完成顺序,第二次在将所有边反向后的图上、按完成顺序的逆序处理顶点。第二次搜索得到的每一棵树就是一个 SCC。算法的时间复杂度为 O(V + E)。

如果把每个 SCC 压缩成一个节点,剩下的就是一个无环有向图。正因如此,SCC 才会出现在依赖分析、死锁检测、2-SAT,以及任何需要先消除环的问题中。

两趟遍历,逐步拆解

  1. 对整个图运行 DFS。当一个顶点完成时(它的所有出边邻居都已处理完),把它压入一个列表。
  2. 将每条边反向,得到转置图。
  3. 从完成列表的末尾向前遍历。对于每个尚未访问的顶点,在转置图上运行 DFS。该搜索能到达的所有顶点构成一个 SCC。

两个循环都必须从每个未访问的顶点开始,而不能只从顶点 0 开始,因为图可能不是连通的。

一个完整示例

取 8 个顶点和以下边:

0 → 1, 1 → 2, 2 → 0      (cycle A: 0, 1, 2)
2 → 3
3 → 4, 4 → 5, 5 → 3      (cycle B: 3, 4, 5)
6 → 5
6 → 7, 7 → 6             (cycle C: 6, 7)

SCC 应该是 2、5 和 7。

第一趟

从 0 开始 DFS,沿着 0 → 1 → 2 → 3 → 4 → 5 前进。顶点 5 没有新的地方可去,所以它最先完成,然后是 4,再是 3。回到 2 时已无路可走,于是 2、1、0 依次完成。顶点 6 尚未访问,从它开始:5 已经完成,于是走到 7,7 完成,然后 6 完成。完成顺序为:

5, 4, 3, 2, 1, 0, 7, 6

第二趟

将每条边反向,从该列表末尾开始处理顶点:先是 6。在反向图中,6 只能到达 7,所以 7 是第一个 SCC。下一个未访问的是 0,在反向图中它能到达 2 和 1,得到 2。边 2 → 3 现在指向 3 → 2,但 2 已被分配,所以搜索在此停止。最后,3 能到达 5 和 4,得到 5。

为什么这样是对的

想象由 SCC 构成的图(每个分量压缩为一个节点),它是无环的。在第一趟中,最后完成的顶点总是属于一个“源”分量,即没有来自其他未分配分量的边指向它。将边反向会把该源变成汇,因此从那里开始的 DFS 无法离开它自己的分量。一旦该分量被移除,同样的论证适用于下一个分量。

在面试中,通常这就是所期望的证明深度:完成顺序选出一个安全的起始分量,而反向边则防止第二次搜索泄漏到其他分量中。

Python 实现

这个版本使用迭代式 DFS,因此在深图上不会触发 Python 的递归限制。

from collections import defaultdict

def kosaraju(n, edges):
    graph = defaultdict(list)
    reverse = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        reverse[v].append(u)

    # Pass 1: record vertices in the order they finish (iterative DFS)
    visited = [False] * n
    order = []
    for start in range(n):
        if visited[start]:
            continue
        visited[start] = True
        stack = [(start, iter(graph[start]))]
        while stack:
            node, neighbors = stack[-1]
            for nxt in neighbors:
                if not visited[nxt]:
                    visited[nxt] = True
                    stack.append((nxt, iter(graph[nxt])))
                    break
            else:
                stack.pop()
                order.append(node)

    # Pass 2: DFS on the reversed graph, latest finish time first
    visited = [False] * n
    components = []
    for start in reversed(order):
        if visited[start]:
            continue
        visited[start] = True
        component = []
        stack = [start]
        while stack:
            node = stack.pop()
            component.append(node)
            for nxt in reverse[node]:
                if not visited[nxt]:
                    visited[nxt] = True
                    stack.append(nxt)
        components.append(component)
    return components


edges = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3), (6, 5), (6, 7), (7, 6)]
print(kosaraju(8, edges))  # [[6, 7], [0, 2, 1], [3, 5, 4]]

每个分量内部顶点的顺序取决于遍历方式,所以在测试中应将分量作为集合来比较。我们在 2,000 个随机图上用暴力可达性检查验证了这个实现。

复杂度

步骤时间
第一次 DFSO(V + E)
构建反向图O(V + E)
第二次 DFSO(V + E)
总计O(V + E)

空间为 O(V + E),用于两个邻接表,外加 O(V) 用于 visited 数组、完成顺序和栈。

会导致出错的常见错误

最常见的 bug 是在 DFS 首次到达顶点时记录它,而不是在它完成时记录。其他错误包括:忘记为第二趟反转完成顺序、在原始图而非反向图上运行第二趟、两趟之间没有清空 visited 数组,以及只从顶点 0 开始导致不连通的部分被跳过。递归 DFS 在长链上还可能栈溢出,这就是上面代码采用迭代式的原因。

好的测试用例:单个顶点、带自环的顶点、一个大环、单向链(每个顶点都是自己的 SCC)、由一条边连接的两个环,以及不连通的图。

Kosaraju 与 Tarjan 对比

两者都在 O(V + E) 时间内运行。Kosaraju 需要两趟并构建反向图,这使它更容易解释、也更容易写对。Tarjan 算法只需一趟且不需要反向图,但要在栈上跟踪发现索引和 low-link 值,需要维护的状态更多。在面试中,用你能正确实现并解释清楚的那个。如果面试官要求单趟完成,那就是在暗示用 Tarjan。

练习图问题

在写代码之前,先手动追踪一遍上面的示例;大多数 bug 都会表现为完成顺序错误。相关指南:Floyd–Warshall 用于全源最短路径,Bellman–Ford 用于含负权边的图,以及我们的 Big O 速查表。如果你在编程练习中使用 YesToTheOffer,它的截图编程帮助可以检查你的思路,而会话记录能显示你是否清晰地解释了推理过程。

通过反馈练习编程面试

截图编程帮助和练习会话的转录。30 分钟免费。

iOS & Android — 即将推出
查看价格