📘 教案 13:拓扑排序(Topological Sort)
一、问题背景
在很多实际问题中,我们并不是单纯处理"数值",而是在处理依赖关系。
例如:
- 课程学习:必须先学 A 才能学 B
- 构建系统:必须先编译库,再编译主程序
- 任务调度:某些任务必须在其他任务完成后才能开始
这些问题可以抽象为一种结构:
有向图中的依赖关系
二、形式化定义
给定一个有向图 ( G = (V, E) ),其中:
- ( V ):顶点(任务)
- ( E ):有向边(依赖关系)
若存在边:
u→v\]\[ u \\to v \]\[u→v
表示:
必须先完成 ( u ),才能执行 ( v )
三、拓扑排序的定义
拓扑排序是:
对有向无环图(DAG)中的节点进行排序,使得所有依赖关系得到满足
换句话说:
如果存在边 ( u \to v ),那么在排序结果中:
u 一定出现在 v 之前\]\[ u \\text{ 一定出现在 } v \\text{ 之前} \]\[u 一定出现在 v 之前
四、关键前提:必须是 DAG
什么是 DAG?
DAG(Directed Acyclic Graph):
有向无环图
为什么必须无环?
如果存在:
text
A → B → C → A
那么:
- A 依赖 B
- B 依赖 C
- C 又依赖 A
👉 没有任何一个可以先执行
因此:
只要存在环,就不存在拓扑排序
五、核心思路
拓扑排序的核心是:
不断找到"没有依赖"的节点,然后删除它们
"没有依赖"是什么意思?
即:
入度=0\]\[ \\text{入度} = 0 \]\[入度=0
六、算法一:Kahn 算法(基于 BFS)
核心步骤
-
统计每个节点的入度
-
将所有入度为 0 的节点加入队列
-
依次处理队列:
- 取出一个节点
- 将其加入结果
- 删除它的所有出边
- 更新邻居节点的入度
-
如果有新的入度为 0 的节点,继续加入队列
代码结构
python
from collections import deque
def topo_sort(graph):
indegree = {v: 0 for v in graph}
# 统计入度
for u in graph:
for v in graph[u]:
indegree[v] += 1
queue = deque([v for v in graph if indegree[v] == 0])
result = []
while queue:
u = queue.popleft()
result.append(u)
for v in graph[u]:
indegree[v] -= 1
if indegree[v] == 0:
queue.append(v)
if len(result) != len(graph):
return "存在环"
return result
七、算法二:DFS 方法
思路
通过 DFS,在"回溯时"记录节点顺序。
关键点
- 访问完所有子节点后,再加入结果
- 最终将结果逆序
代码结构
python
def topo_sort_dfs(graph):
visited = set()
result = []
def dfs(node):
if node in visited:
return
visited.add(node)
for nei in graph[node]:
dfs(nei)
result.append(node)
for node in graph:
dfs(node)
return result[::-1]
八、两种方法对比
| 方法 | 思路 | 特点 |
|---|---|---|
| Kahn(BFS) | 入度为0开始 | 易理解,能检测环 |
| DFS | 后序遍历 | 更偏递归结构 |
九、时间复杂度
O(V+E)\]\[ O(V + E) \]\[O(V+E)
- 每个节点访问一次
- 每条边处理一次
十、典型应用
1. 课程安排(经典题)
判断是否可以完成所有课程
2. 构建系统(编译顺序)
3. 任务调度
4. 数据处理依赖
十一、常见错误
- 忘记判断是否有环(结果长度不等于节点数)
- 入度更新错误
- 图结构构建错误
十二、本质总结
拓扑排序解决的问题不是"排序大小",而是:
在一组存在依赖关系的元素中,找到一个合法执行顺序
更适合教学的一句话表达
拓扑排序是在有向无环图中,按照依赖关系对节点进行线性排序,使得所有前置条件都在使用之前被满足。