1. 什么是拓扑排序?
拓扑排序针对的是 有向无环图(DAG,Directed Acyclic Graph)。
假设有这样的依赖关系:
A → B
A → C
B → D
C → D
箭头 A → B 可以理解为:
想做 B,必须先做 A。
那么一种合法的执行顺序就是:
A → B → C → D
也可以是:
A → C → B → D
这两个都是正确的。
所以,拓扑排序就是把图中的所有节点排成一个线性顺序,使得对于每一条边 u → v,u 都出现在 v 前面。
注意:拓扑排序通常 不唯一。
2. 为什么必须是“有向无环图”?
比如:
A → B
↑ ↓
└── C
也就是:
A → B
B → C
C → A
A 要在 B 前面,B 要在 C 前面,C 又要在 A 前面。
这是不可能的。
一个有向图存在拓扑排序,当且仅当它是 DAG(Directed Acyclic Graph)。
这个性质非常重要,算法题会反过来利用拓扑排序 判断图中是否存在环。
3. 最经典的方法:入度法(Kahn 算法)
Kahn 算法的核心流程可以概括为:
- 统计所有节点的入度。
- 把所有 入度为 0 的节点放入队列。
- 每次从队列取出一个节点,将它加入拓扑序列。
- 删除这个节点发出的所有边,也就是让它指向的节点入度
-1。 - 如果某个节点入度变成 0,就加入队列。
- 重复直到队列为空。
例如:
A → B
A → C
B → D
C → D
初始入度:
A = 0
B = 1
C = 1
D = 2
所以首先只有:
队列:[A]
取出 A:
结果:[A]
B 入度:1 → 0
C 入度:1 → 0
于是:
队列:[B, C]
假设取 B:
结果:[A, B]
D 入度:2 → 1
再取 C:
结果:[A, B, C]
D 入度:1 → 0
然后 D 入队,最终:
[A, B, C, D]
这就是一个拓扑序列
4. Kahn 算法代码
以常见的邻接表写法为例:
bool TopologicalSort(ALGraph& G) {
int indegree[MAXV] = {0};
//遍历领接表,获得每个节点的入度
for (int i = 0; i < G.vexnum; ++i) {
for (ArcNode* p = G.vertices[i].first; p != nullptr; p = p->next) {
indegree[p->adjvex]++;
}
}
std::queue<int> q;
//将入度为0的节点压入队列
for (int i = 0; i < G.vexnum; ++i) {
if (indegree[i] == 0) {
q.push(i);
}
}
int count = 0;
//循环对每个度为0的节点进行操作
//直到队列为空
while (!q.empty()) {
int v = q.front();
q.pop();
count++;
std::cout << v << " ";
//队列中的节点,让其指向的节点入度-1
//如果某个节点入度为0,压入队列中
for (ArcNode* p = G.vertices[v].first; p != nullptr; p = p->next) {
int k = p->adjvex;
indegree[k]--;
if (indegree[k] == 0) {
q.push(k);
}
}
}
return count == G.vexnum;
}
为什么 count != G.vexnum 就说明有环?
这是拓扑排序里非常经典的判断。
考虑:
A → B → C
↑ ↓
└───┘
其中:
B → C
C → B
B 和 C 形成了一个环。
那么:
B 入度 > 0
C 入度 > 0
即使其他节点都处理完了,B 和 C 的入度也永远不可能变成 0。
于是队列最终会空掉,但是还有节点没有处理。
因此:
count < G.vexnum
说明图中存在环
反过来,如果:
count == G.vexnum
说明所有节点都成功被处理,因此图是 DAG。
5. 时间复杂度
V = 节点数
E = 边数
每个节点最多处理一次,每条边最多处理一次,所以时间复杂度是:
拓扑排序的额外空间复杂度是
如果把输入图的邻接表存储也算上,则总空间复杂度是 。
今天学校开始正式上课了,
让人头疼的老师,
考勤抓的严,课堂作业多。
课上根本没法集中注意力学习,学不进去。
我的学习计划迎来重大挑战😡。
对可恶的上课勇敢说不
