Skip to content

Commit c858236

Browse files
committed
fix(post): error parse in link
1 parent 9ff4aac commit c858236

1 file changed

Lines changed: 131 additions & 131 deletions

File tree

src/content/blog/2025-04-02/index.md

Lines changed: 131 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -7,130 +7,130 @@ latex: true
77
pdf: true
88
---
99

10-
## 引言
11-
在计算机科学中,拓扑排序是一种解决依赖关系问题的关键算法。想象这样一个场景:大学选课时,某些课程需要先修课程。例如,学习「数据结构」前必须先修「程序设计基础」,这种依赖关系构成一个有向无环图(DAG)。拓扑排序的作用正是为这类依赖关系找到一种合理的执行顺序。本文将深入解析拓扑排序的核心原理,并通过 Python 代码实现两种经典算法。
10+
## 引言
11+
在计算机科学中,拓扑排序是一种解决依赖关系问题的关键算法。想象这样一个场景:大学选课时,某些课程需要先修课程。例如,学习「数据结构」前必须先修「程序设计基础」,这种依赖关系构成一个有向无环图(DAG)。拓扑排序的作用正是为这类依赖关系找到一种合理的执行顺序。本文将深入解析拓扑排序的核心原理,并通过 Python 代码实现两种经典算法。
1212

13-
## 拓扑排序基础概念
14-
拓扑排序的定义是:对 DAG 的顶点进行线性排序,使得对于任意有向边 $u \to v$,顶点 $u$ 在排序中都出现在顶点 $v$ 之前。例如,若图中存在边 $A \to B$ 和 $B \to C$,则可能的排序之一是 $[A, B, C]$。
13+
## 拓扑排序基础概念
14+
拓扑排序的定义是:对 DAG 的顶点进行线性排序,使得对于任意有向边 $u \to v$,顶点 $u$ 在排序中都出现在顶点 $v$ 之前。例如,若图中存在边 $A \to B$ 和 $B \to C$,则可能的排序之一是 $[A, B, C]$。
1515

16-
拓扑排序有两个关键特性:
17-
1. **无环性**:若图中存在环(例如 $A \to B \to C \to A$),则无法进行拓扑排序。可通过深度优先搜索(DFS)检测环的存在。
18-
2. **不唯一性**:同一 DAG 可能有多种有效排序。例如,若图中有两个无依赖关系的节点 $A$ 和 $B$,则 $[A, B]$ 和 $[B, A]$ 均为合法结果。
16+
拓扑排序有两个关键特性:
17+
1. **无环性**:若图中存在环(例如 $A \to B \to C \to A$),则无法进行拓扑排序。可通过深度优先搜索(DFS)检测环的存在。
18+
2. **不唯一性**:同一 DAG 可能有多种有效排序。例如,若图中有两个无依赖关系的节点 $A$ 和 $B$,则 $[A, B]$ 和 $[B, A]$ 均为合法结果。
1919

20-
## 拓扑排序算法原理
20+
## 拓扑排序算法原理
2121

22-
### Kahn 算法(基于入度)
23-
Kahn 算法的核心思想是不断移除入度为 0 的节点,直到所有节点被处理。具体步骤如下:
24-
1. 初始化所有节点的入度表。
25-
2. 将入度为 0 的节点加入队列。
26-
3. 依次处理队列中的节点,将其邻接节点的入度减 1。若邻接节点入度变为 0,则加入队列。
27-
4. 若最终处理的节点数等于总节点数,则排序成功;否则说明图中存在环。
22+
### Kahn 算法(基于入度)
23+
Kahn 算法的核心思想是不断移除入度为 0 的节点,直到所有节点被处理。具体步骤如下:
24+
1. 初始化所有节点的入度表。
25+
2. 将入度为 0 的节点加入队列。
26+
3. 依次处理队列中的节点,将其邻接节点的入度减 1。若邻接节点入度变为 0,则加入队列。
27+
4. 若最终处理的节点数等于总节点数,则排序成功;否则说明图中存在环。
2828

29-
该算法依赖队列数据结构,时间复杂度为 $O(V + E)$,其中 $V$ 是节点数,$E$ 是边数。
29+
该算法依赖队列数据结构,时间复杂度为 $O(V + E)$,其中 $V$ 是节点数,$E$ 是边数。
3030

31-
### DFS 后序遍历法
32-
DFS 算法通过深度优先遍历图,并按递归完成时间的逆序得到拓扑排序。具体步骤如下:
33-
1. 从任意未访问节点开始递归 DFS。
34-
2. 将当前节点标记为已访问。
35-
3. 递归处理所有邻接节点。
36-
4. 递归结束后将当前节点压入栈中。
37-
5. 最终栈顶到栈底的顺序即为拓扑排序结果。
31+
### DFS 后序遍历法
32+
DFS 算法通过深度优先遍历图,并按递归完成时间的逆序得到拓扑排序。具体步骤如下:
33+
1. 从任意未访问节点开始递归 DFS。
34+
2. 将当前节点标记为已访问。
35+
3. 递归处理所有邻接节点。
36+
4. 递归结束后将当前节点压入栈中。
37+
5. 最终栈顶到栈底的顺序即为拓扑排序结果。
3838

39-
DFS 算法同样具有 $O(V + E)$ 的时间复杂度,但需要额外的栈空间存储结果。
39+
DFS 算法同样具有 $O(V + E)$ 的时间复杂度,但需要额外的栈空间存储结果。
4040

41-
### 算法对比
42-
- **Kahn 算法**:显式利用入度信息,适合动态调整入度的场景(如动态图)。
43-
- **DFS 算法**:代码简洁,但难以处理动态变化的图。
41+
### 算法对比
42+
- **Kahn 算法**:显式利用入度信息,适合动态调整入度的场景(如动态图)。
43+
- **DFS 算法**:代码简洁,但难以处理动态变化的图。
4444

45-
## 代码实现(以 Python 为例)
45+
## 代码实现(以 Python 为例)
4646

47-
### 图的表示
48-
使用邻接表表示图,例如节点 0 的邻接节点为 [1, 2]
49-
```python
47+
### 图的表示
48+
使用邻接表表示图,例如节点 0 的邻接节点为 [1, 2]
49+
```python
5050
graph = {
5151
0: [1, 2],
5252
1: [3],
5353
2: [3],
5454
3: []
5555
}
56-
```
57-
58-
### Kahn 算法实现
59-
```python
60-
from collections import deque
61-
62-
def topological_sort_kahn(graph, n):
63-
# 初始化入度表
64-
in_degree = {i: 0 for i in range(n)}
65-
for u in graph:
66-
for v in graph[u]:
67-
in_degree[v] += 1
68-
69-
# 将入度为 0 的节点加入队列
70-
queue = deque([u for u in in_degree if in_degree[u] == 0])
71-
result = []
72-
73-
while queue:
74-
u = queue.popleft()
75-
result.append(u)
76-
# 更新邻接节点的入度
77-
for v in graph.get(u, []):
78-
in_degree[v] -= 1
79-
if in_degree[v] == 0:
80-
queue.append(v)
81-
82-
# 检查是否存在环
83-
if len(result) != n:
84-
return [] # 存在环
85-
return result
86-
```
87-
88-
**代码解读**
89-
- `in_degree` 字典记录每个节点的入度。
90-
- 队列 `queue` 维护当前入度为 0 的节点。
91-
- 每次从队列取出节点后,将其邻接节点的入度减 1。若邻接节点入度变为 0,则加入队列。
92-
- 最终若结果列表长度不等于节点总数,则说明存在环。
93-
94-
### DFS 算法实现
95-
```python
96-
def topological_sort_dfs(graph):
97-
visited = set()
98-
stack = []
99-
100-
def dfs(u):
101-
if u in visited:
102-
return
103-
visited.add(u)
104-
# 递归访问所有邻接节点
105-
for v in graph.get(u, []):
106-
dfs(v)
107-
# 递归结束后压入栈
108-
stack.append(u)
109-
110-
for u in graph:
111-
if u not in visited:
112-
dfs(u)
113-
# 逆序输出栈
114-
return stack[::-1]
115-
```
116-
117-
**代码解读**
118-
- `visited` 集合记录已访问的节点。
119-
- `dfs` 函数递归访问邻接节点,完成后将当前节点压入栈。
120-
- 最终栈的逆序即为拓扑排序结果(后进先出的栈结构需要反转)。
121-
122-
## 实例演示与测试
123-
假设有以下 DAG:
124-
```
125-
5 → 0 ← 4
126-
↓ ↓ ↓
127-
2 → 3 → 1
128-
```
129-
130-
**手动推导**:可能的拓扑排序为 `[5, 4, 2, 0, 3, 1]`
131-
**代码测试**
132-
- 输入图的邻接表表示:
133-
```python
56+
```
57+
58+
### Kahn 算法实现
59+
```python
60+
from collections import deque
61+
62+
def topological_sort_kahn(graph, n):
63+
# 初始化入度表
64+
in_degree = {i: 0 for i in range(n)}
65+
for u in graph:
66+
for v in graph[u]:
67+
in_degree[v] += 1
68+
69+
# 将入度为 0 的节点加入队列
70+
queue = deque([u for u in in_degree if in_degree[u] == 0])
71+
result = []
72+
73+
while queue:
74+
u = queue.popleft()
75+
result.append(u)
76+
# 更新邻接节点的入度
77+
for v in graph.get(u, []):
78+
in_degree[v] -= 1
79+
if in_degree[v] == 0:
80+
queue.append(v)
81+
82+
# 检查是否存在环
83+
if len(result) != n:
84+
return [] # 存在环
85+
return result
86+
```
87+
88+
**代码解读**
89+
- `in_degree` 字典记录每个节点的入度。
90+
- 队列 `queue` 维护当前入度为 0 的节点。
91+
- 每次从队列取出节点后,将其邻接节点的入度减 1。若邻接节点入度变为 0,则加入队列。
92+
- 最终若结果列表长度不等于节点总数,则说明存在环。
93+
94+
### DFS 算法实现
95+
```python
96+
def topological_sort_dfs(graph):
97+
visited = set()
98+
stack = []
99+
100+
def dfs(u):
101+
if u in visited:
102+
return
103+
visited.add(u)
104+
# 递归访问所有邻接节点
105+
for v in graph.get(u, []):
106+
dfs(v)
107+
# 递归结束后压入栈
108+
stack.append(u)
109+
110+
for u in graph:
111+
if u not in visited:
112+
dfs(u)
113+
# 逆序输出栈
114+
return stack[::-1]
115+
```
116+
117+
**代码解读**
118+
- `visited` 集合记录已访问的节点。
119+
- `dfs` 函数递归访问邻接节点,完成后将当前节点压入栈。
120+
- 最终栈的逆序即为拓扑排序结果(后进先出的栈结构需要反转)。
121+
122+
## 实例演示与测试
123+
假设有以下 DAG:
124+
```
125+
5 → 0 ← 4
126+
↓ ↓ ↓
127+
2 → 3 → 1
128+
```
129+
130+
**手动推导**:可能的拓扑排序为 `[5, 4, 2, 0, 3, 1]`
131+
**代码测试**
132+
- 输入图的邻接表表示:
133+
```python
134134
graph = {
135135
5: [0, 2],
136136
4: [0, 1],
@@ -139,26 +139,26 @@ graph = {
139139
3: [1],
140140
1: []
141141
}
142-
n = 6
143-
```
144-
- 运行 `topological_sort_kahn(graph, 6)` 应返回长度为 6 的合法排序。
145-
- 若图中存在环(例如添加边 `1 → 5`),两种算法均返回空列表。
146-
147-
## 复杂度与优化
148-
两种算法的时间复杂度均为 $O(V + E)$,空间复杂度为 $O(V)$。
149-
**优化技巧**:若需要字典序最小的排序,可将 Kahn 算法中的队列替换为优先队列(最小堆)。
150-
151-
## 实际应用场景
152-
1. **编译器构建**:确定源代码文件的编译顺序。
153-
2. **课程安排**:解决 LeetCode 210 题「课程表 II」的依赖问题。
154-
3. **任务调度**:管理具有前后依赖关系的任务执行顺序。
155-
156-
## 总结与扩展
157-
拓扑排序是处理依赖关系的核心算法。通过 Kahn 算法和 DFS 算法的对比,可根据实际需求选择实现方式。进一步学习可探索:
158-
- **强连通分量**:使用 Tarjan 算法识别图中的环。
159-
- **动态拓扑排序**:在频繁增删边的场景下维护排序结果。
160-
- **练习题**:LeetCode 207(判断能否完成课程)、310(最小高度树)等。
161-
162-
## 参考资源
163-
- 《算法导论》第 22.4 章「拓扑排序」。
164-
- VisuAlgo 的可视化工具:https://visualgo.net/zh/graphds
142+
n = 6
143+
```
144+
- 运行 `topological_sort_kahn(graph, 6)` 应返回长度为 6 的合法排序。
145+
- 若图中存在环(例如添加边 `1 → 5`),两种算法均返回空列表。
146+
147+
## 复杂度与优化
148+
两种算法的时间复杂度均为 $O(V + E)$,空间复杂度为 $O(V)$。
149+
**优化技巧**:若需要字典序最小的排序,可将 Kahn 算法中的队列替换为优先队列(最小堆)。
150+
151+
## 实际应用场景
152+
1. **编译器构建**:确定源代码文件的编译顺序。
153+
2. **课程安排**:解决 LeetCode 210 题「课程表 II」的依赖问题。
154+
3. **任务调度**:管理具有前后依赖关系的任务执行顺序。
155+
156+
## 总结与扩展
157+
拓扑排序是处理依赖关系的核心算法。通过 Kahn 算法和 DFS 算法的对比,可根据实际需求选择实现方式。进一步学习可探索:
158+
- **强连通分量**:使用 Tarjan 算法识别图中的环。
159+
- **动态拓扑排序**:在频繁增删边的场景下维护排序结果。
160+
- **练习题**:LeetCode 207(判断能否完成课程)、310(最小高度树)等。
161+
162+
## 参考资源
163+
- 《算法导论》第 22.4 章「拓扑排序」。
164+
- VisuAlgo 的可视化工具:https://visualgo.net/zh/graphds

0 commit comments

Comments
 (0)