Menu
CoddyTech

拓扑排序

最后更新

有向无环图(DAG)的拓扑排序是把所有节点排成一个线性序列,使得对于每条边 u → v,u 都排在 v 前面。它回答的是这类问题:“这些任务按什么顺序执行,才能保证每个前置任务都先完成?”点击上方的“播放”,观看 Kahn 算法如何按合法的顺序把节点逐个剥离出来。

Kahn 算法反复取出一个没有剩余入边的节点(入度为 0),把它追加到序列中,再删除它的所有出边,这可能会让新的节点入度变为 0。它只适用于 DAG:如果存在环,有些节点的入度永远到不了 0,也就不存在合法的顺序。它的时间复杂度是 O(V + E)。

时间复杂度与空间复杂度

指标复杂度说明
时间O(V + E)每个节点输出一次,每条边删除一次
空间O(V)入度计数 + 就绪集合 + 结果序列
前提DAG有环的图没有拓扑序列
结果不唯一可能存在多个合法顺序

分步说明(Kahn 算法)

步骤发生了什么
1计算每个节点的入度(指向它的边数)。
2把所有入度为 0 的节点放进就绪集合。
3取出一个就绪节点,追加到输出序列中。
4把它每个后继节点的入度减一。
5入度变为 0 的后继节点加入就绪集合。
6重复以上步骤,直到就绪集合为空。

实例演示

对边为 A→C、B→C、C→D、C→E、D→F、E→F 的 DAG 排序(初始入度 A:0 B:0 C:2 D:1 E:1 F:2):

步骤就绪集合序列操作
0{A, B}[]A 和 B 的初始入度都是 0,所以两者都已就绪。
1{B}[A]输出 A;它的边 A→C 让 C 的入度从 2 降到 1。
2{C}[A, B]输出 B;它的边 B→C 让 C 从 1 降到 0,C 就绪。
3{D, E}[A, B, C]输出 C;边 C→D 和 C→E 让 D 和 E 降到 0,两者都已就绪。
4{E}[A, B, C, D]输出 D;它的边 D→F 让 F 的入度从 2 降到 1。
5{F}[A, B, C, D, E]输出 E;它的边 E→F 让 F 从 1 降到 0,F 就绪。
6{}[A, B, C, D, E, F]输出 F;就绪集合为空,6 个节点全部排好,完成。

什么时候用拓扑排序

适合使用不适合使用
你需要一个满足依赖关系的顺序(构建步骤、安装软件包、课程先修关系)。图是无向的:拓扑序只对有向图有定义。
图是 DAG,你只需要任意一个合法的线性顺序。图中可能有环,而你仍然需要一个全序(这种顺序不存在)。
你想低成本地检测环:拓扑排序失败就证明存在环。你需要按某种权重求最短或最优的顺序;普通的拓扑排序会忽略权重。
你只需用 O(V + E) 处理一次排序。边不断变化,每次更新都要重新排序,这时增量式的数据结构更合适。

Topological Sort 代码

简洁、可运行的 Topological Sort 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。

Topological Sort 的 Python 代码

Python
1from collections import deque2
3
4def topological_sort(graph):5    # Kahn's algorithm: repeatedly remove nodes with no incoming edges6    in_degree = {node: 0 for node in graph}7    for node in graph:8        for neighbor in graph[node]:9            in_degree[neighbor] += 110    queue = deque(node for node in graph if in_degree[node] == 0)11    order = []12    while queue:13        node = queue.popleft()14        order.append(node)15        for neighbor in graph[node]:16            in_degree[neighbor] -= 117            if in_degree[neighbor] == 0:18                queue.append(neighbor)19    if len(order) != len(graph):20        raise ValueError("Graph has a cycle, no topological order")21    return order22
23
24graph = {25    "shirt": ["tie", "jacket"],26    "tie": ["jacket"],27    "pants": ["shoes", "jacket"],28    "socks": ["shoes"],29    "shoes": [],30    "jacket": [],31}32
33print(" -> ".join(topological_sort(graph)))
在 Python 在线编辑器中运行这段代码

拓扑排序常见问题

拓扑排序有什么用?

它给任务排序,保证每个依赖项都排在需要它的任务之前。实际应用包括构建系统和包管理器(先编译依赖项)、有先修要求的课程安排,以及电子表格公式的计算顺序。

拓扑排序的时间复杂度是多少?
Kahn 算法和基于 DFS 的方法都是 O(V + E),因为每个节点处理一次,每条边检查一次。它们都需要 O(V) 的额外空间。
拓扑排序为什么要求是 DAG?
有向环会造成矛盾:如果 a 必须在 b 之前,而 b 又必须在 a 之前,就没有任何线性顺序能同时满足。所以拓扑序列存在,当且仅当图是有向无环图。如果 Kahn 算法结束时还有节点没有输出,就说明检测到了环。
Kahn 算法和 DFS 拓扑排序有什么区别?
Kahn 算法是迭代的,类似 BFS:它反复删除入度为 0 的节点,让环检测和排序过程都一目了然。DFS 方法递归地访问节点,在每个节点的递归结束时把它插到序列最前面,得到的是完成时间的逆序。两者都是 O(V + E);Kahn 算法避免了深层递归,也能自然地给出就绪集合,而 DFS 写起来通常更短。
什么时候该用拓扑排序,而不是普通排序?
当顺序由元素之间的依赖关系决定,而不是由可比较的键决定时,就用拓扑排序。像 O(n log n) 的归并排序这样的普通比较排序按值排序;拓扑排序按“必须排在前面”的边排序,而且和比较排序不同,同一个输入可以有多个合法答案。
拓扑排序的结果是唯一的吗?

通常不是。只要同时有两个或更多节点就绪(入度为 0),你就可以按任意顺序输出它们,所以大多数 DAG 都有好几个合法的拓扑序列。只有每一步都恰好只有一个就绪节点时,顺序才是唯一的,这种情况发生在 DAG 构成一条单链(哈密顿路径)时。

Coddy 编程语言插图

在 Coddy 掌握算法

开始