拓扑排序
最后更新
有向无环图(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 代码
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)))Topological Sort 的 JavaScript 代码
1const graph = {2 A: ["C"],3 B: ["C", "D"],4 C: ["E"],5 D: ["F"],6 E: ["F"],7 F: [],8};9
10// Kahn algorithm: repeatedly take a node with no incoming edges11function topologicalSort() {12 const inDegree = {};13 for (const node in graph) inDegree[node] = 0;14 for (const node in graph) {15 for (const next of graph[node]) inDegree[next]++;16 }17 const queue = Object.keys(inDegree).filter((n) => inDegree[n] === 0);18 const order = [];19 while (queue.length > 0) {20 const node = queue.shift();21 order.push(node);22 for (const next of graph[node]) {23 if (--inDegree[next] === 0) queue.push(next);24 }25 }26 if (order.length !== Object.keys(graph).length) {27 throw new Error("Graph has a cycle");28 }29 return order;30}31
32console.log("Topological order:", topologicalSort().join(" -> "));Topological Sort 的 Java 代码
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 int n = 6;8 // DAG: an edge u -> v means u must come before v9 List<List<Integer>> adj = List.of(10 List.of(2),11 List.of(2, 3),12 List.of(4),13 List.of(4),14 List.of(5),15 List.of()16 );17 int[] inDegree = new int[n];18 for (List<Integer> next : adj) {19 for (int v : next) inDegree[v]++;20 }21
22 // Kahn: start from every node with no prerequisites23 Queue<Integer> queue = new ArrayDeque<>();24 for (int i = 0; i < n; i++) {25 if (inDegree[i] == 0) queue.add(i);26 }27
28 StringBuilder order = new StringBuilder("Topological order:");29 while (!queue.isEmpty()) {30 int node = queue.poll();31 order.append(" ").append(node);32 for (int next : adj.get(node)) {33 if (--inDegree[next] == 0) queue.add(next);34 }35 }36 System.out.println(order);37 }38}Topological Sort 的 C++ 代码
1#include <iostream>2#include <queue>3#include <vector>4
5int main() {6 // Directed acyclic graph: adj[u] lists nodes that depend on u7 std::vector<std::vector<int>> adj = {8 {}, // 09 {}, // 110 {3}, // 2 -> 311 {1}, // 3 -> 112 {0, 1}, // 4 -> 0, 113 {0, 2}, // 5 -> 0, 214 };15 int n = static_cast<int>(adj.size());16 std::vector<int> inDegree(n, 0);17 for (const auto& neighbors : adj) {18 for (int v : neighbors) ++inDegree[v];19 }20 // Kahn's algorithm: start from nodes with no incoming edges21 std::queue<int> ready;22 for (int u = 0; u < n; ++u) {23 if (inDegree[u] == 0) ready.push(u);24 }25 std::cout << "Topological order: ";26 while (!ready.empty()) {27 int u = ready.front();28 ready.pop();29 std::cout << u << " ";30 // Removing u unlocks neighbors whose in-degree drops to 031 for (int v : adj[u]) {32 if (--inDegree[v] == 0) ready.push(v);33 }34 }35 std::cout << "\n";36 return 0;37}Topological Sort 的 C 代码
1#include <stdio.h>2
3#define N 64
5int main(void) {6 // Directed acyclic graph: adj[u][v] = 1 means an edge u -> v7 int adj[N][N] = {0};8 adj[2][3] = 1;9 adj[3][1] = 1;10 adj[4][0] = 1;11 adj[4][1] = 1;12 adj[5][0] = 1;13 adj[5][2] = 1;14 int inDegree[N] = {0};15 for (int u = 0; u < N; u++) {16 for (int v = 0; v < N; v++) inDegree[v] += adj[u][v];17 }18 // Kahn's algorithm: start from nodes with no incoming edges19 int queue[N];20 int head = 0, tail = 0;21 for (int u = 0; u < N; u++) {22 if (inDegree[u] == 0) queue[tail++] = u;23 }24 printf("Topological order: ");25 while (head < tail) {26 int u = queue[head++];27 printf("%d ", u);28 // Removing u unlocks neighbors whose in-degree drops to 029 for (int v = 0; v < N; v++) {30 if (adj[u][v] && --inDegree[v] == 0) queue[tail++] = v;31 }32 }33 printf("\n");34 return 0;35}拓扑排序常见问题
拓扑排序有什么用?
它给任务排序,保证每个依赖项都排在需要它的任务之前。实际应用包括构建系统和包管理器(先编译依赖项)、有先修要求的课程安排,以及电子表格公式的计算顺序。
拓扑排序的时间复杂度是多少?
O(V + E),因为每个节点处理一次,每条边检查一次。它们都需要 O(V) 的额外空间。拓扑排序为什么要求是 DAG?
a 必须在 b 之前,而 b 又必须在 a 之前,就没有任何线性顺序能同时满足。所以拓扑序列存在,当且仅当图是有向无环图。如果 Kahn 算法结束时还有节点没有输出,就说明检测到了环。Kahn 算法和 DFS 拓扑排序有什么区别?
O(V + E);Kahn 算法避免了深层递归,也能自然地给出就绪集合,而 DFS 写起来通常更短。什么时候该用拓扑排序,而不是普通排序?
O(n log n) 的归并排序这样的普通比较排序按值排序;拓扑排序按“必须排在前面”的边排序,而且和比较排序不同,同一个输入可以有多个合法答案。拓扑排序的结果是唯一的吗?
通常不是。只要同时有两个或更多节点就绪(入度为 0),你就可以按任意顺序输出它们,所以大多数 DAG 都有好几个合法的拓扑序列。只有每一步都恰好只有一个就绪节点时,顺序才是唯一的,这种情况发生在 DAG 构成一条单链(哈密顿路径)时。