Skip to content

Commit cb40e04

Browse files
chore: automated publish
1 parent f17c582 commit cb40e04

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed
262 KB
Binary file not shown.

public/blog/2025-04-02/index.pdf

115 KB
Binary file not shown.

public/blog/2025-04-02/index.tex

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
\title{"理解并实现基本的拓扑排序算法:从理论到代码实践"}
2+
\author{"杨其臻"}
3+
\date{"Apr 02, 2025"}
4+
\maketitle
5+
\chapter{引言}
6+
在计算机科学中,拓扑排序是一种解决依赖关系问题的关键算法。想象这样一个场景:大学选课时,某些课程需要先修课程。例如,学习「数据结构」前必须先修「程序设计基础」,这种依赖关系构成一个有向无环图(DAG)。拓扑排序的作用正是为这类依赖关系找到一种合理的执行顺序。本文将深入解析拓扑排序的核心原理,并通过 Python 代码实现两种经典算法。\par
7+
\chapter{拓扑排序基础概念}
8+
拓扑排序的定义是:对 DAG 的顶点进行线性排序,使得对于任意有向边 $u \to v$,顶点 $u$ 在排序中都出现在顶点 $v$ 之前。例如,若图中存在边 $A \to B$$B \to C$,则可能的排序之一是 $[A, B, C]$\par
9+
拓扑排序有两个关键特性:\par
10+
\begin{itemize}
11+
\item \textbf{无环性}:若图中存在环(例如 $A \to B \to C \to A$),则无法进行拓扑排序。可通过深度优先搜索(DFS)检测环的存在。
12+
\item \textbf{不唯一性}:同一 DAG 可能有多种有效排序。例如,若图中有两个无依赖关系的节点 $A$$B$,则 $[A, B]$$[B, A]$ 均为合法结果。
13+
\end{itemize}
14+
\chapter{拓扑排序算法原理}
15+
\section{Kahn 算法(基于入度)}
16+
Kahn 算法的核心思想是不断移除入度为 0 的节点,直到所有节点被处理。具体步骤如下:\par
17+
\begin{itemize}
18+
\item 初始化所有节点的入度表。
19+
\item 将入度为 0 的节点加入队列。
20+
\item 依次处理队列中的节点,将其邻接节点的入度减 1。若邻接节点入度变为 0,则加入队列。
21+
\item 若最终处理的节点数等于总节点数,则排序成功;否则说明图中存在环。
22+
\end{itemize}
23+
该算法依赖队列数据结构,时间复杂度为 $O(V + E)$,其中 $V$ 是节点数,$E$ 是边数。\par
24+
\section{DFS 后序遍历法}
25+
DFS 算法通过深度优先遍历图,并按递归完成时间的逆序得到拓扑排序。具体步骤如下:\par
26+
\begin{itemize}
27+
\item 从任意未访问节点开始递归 DFS。
28+
\item 将当前节点标记为已访问。
29+
\item 递归处理所有邻接节点。
30+
\item 递归结束后将当前节点压入栈中。
31+
\item 最终栈顶到栈底的顺序即为拓扑排序结果。
32+
\end{itemize}
33+
DFS 算法同样具有 $O(V + E)$ 的时间复杂度,但需要额外的栈空间存储结果。\par
34+
\section{算法对比}
35+
\begin{enumerate}
36+
\item \textbf{Kahn 算法}:显式利用入度信息,适合动态调整入度的场景(如动态图)。
37+
\item \textbf{DFS 算法}:代码简洁,但难以处理动态变化的图。
38+
\end{enumerate}
39+
\chapter{代码实现(以 Python 为例)}
40+
\section{图的表示}
41+
使用邻接表表示图,例如节点 0 的邻接节点为 [1, 2]:\par
42+
\begin{lstlisting}[language=python]
43+
graph = {
44+
0: [1, 2],
45+
1: [3],
46+
2: [3],
47+
3: []
48+
}
49+
\end{lstlisting}
50+
\section{Kahn 算法实现}
51+
\begin{lstlisting}[language=python]
52+
from collections import deque
53+
54+
def topological_sort_kahn(graph, n):
55+
# 初始化入度表
56+
in_degree = {i: 0 for i in range(n)}
57+
for u in graph:
58+
for v in graph[u]:
59+
in_degree[v] += 1
60+
61+
# 将入度为 0 的节点加入队列
62+
queue = deque([u for u in in_degree if in_degree[u] == 0])
63+
result = []
64+
65+
while queue:
66+
u = queue.popleft()
67+
result.append(u)
68+
# 更新邻接节点的入度
69+
for v in graph.get(u, []):
70+
in_degree[v] -= 1
71+
if in_degree[v] == 0:
72+
queue.append(v)
73+
74+
# 检查是否存在环
75+
if len(result) != n:
76+
return [] # 存在环
77+
return result
78+
\end{lstlisting}
79+
\textbf{代码解读}:\par
80+
\begin{enumerate}
81+
\item \verb!in_degree! 字典记录每个节点的入度。
82+
\item 队列 \verb!queue! 维护当前入度为 0 的节点。
83+
\item 每次从队列取出节点后,将其邻接节点的入度减 1。若邻接节点入度变为 0,则加入队列。
84+
\item 最终若结果列表长度不等于节点总数,则说明存在环。
85+
\end{enumerate}
86+
\section{DFS 算法实现}
87+
\begin{lstlisting}[language=python]
88+
def topological_sort_dfs(graph):
89+
visited = set()
90+
stack = []
91+
92+
def dfs(u):
93+
if u in visited:
94+
return
95+
visited.add(u)
96+
# 递归访问所有邻接节点
97+
for v in graph.get(u, []):
98+
dfs(v)
99+
# 递归结束后压入栈
100+
stack.append(u)
101+
102+
for u in graph:
103+
if u not in visited:
104+
dfs(u)
105+
# 逆序输出栈
106+
return stack[::-1]
107+
\end{lstlisting}
108+
\textbf{代码解读}:\par
109+
\begin{enumerate}
110+
\item \verb!visited! 集合记录已访问的节点。
111+
\item \verb!dfs! 函数递归访问邻接节点,完成后将当前节点压入栈。
112+
\item 最终栈的逆序即为拓扑排序结果(后进先出的栈结构需要反转)。
113+
\end{enumerate}
114+
\chapter{实例演示与测试}
115+
假设有以下 DAG:\par
116+
\begin{lstlisting}
117+
5 → 0 ← 4
118+
↓ ↓ ↓
119+
2 → 3 → 1
120+
\end{lstlisting}
121+
\textbf{手动推导}:可能的拓扑排序为 \verb![5, 4, 2, 0, 3, 1]!。\par\textbf{代码测试}:\par
122+
\begin{enumerate}
123+
\item 输入图的邻接表表示:
124+
\end{enumerate}
125+
\begin{lstlisting}[language=python]
126+
graph = {
127+
5: [0, 2],
128+
4: [0, 1],
129+
2: [3],
130+
0: [3],
131+
3: [1],
132+
1: []
133+
}
134+
n = 6
135+
\end{lstlisting}
136+
\begin{enumerate}
137+
\item 运行 \verb!topological_sort_kahn(graph, 6)! 应返回长度为 6 的合法排序。
138+
\item 若图中存在环(例如添加边 \verb!1 → 5!),两种算法均返回空列表。
139+
\end{enumerate}
140+
\chapter{复杂度与优化}
141+
两种算法的时间复杂度均为 $O(V + E)$,空间复杂度为 $O(V)$\par\textbf{优化技巧}:若需要字典序最小的排序,可将 Kahn 算法中的队列替换为优先队列(最小堆)。\par
142+
\chapter{实际应用场景}
143+
\begin{itemize}
144+
\item \textbf{编译器构建}:确定源代码文件的编译顺序。
145+
\item \textbf{课程安排}:解决 LeetCode 210 题「课程表 II」的依赖问题。
146+
\item \textbf{任务调度}:管理具有前后依赖关系的任务执行顺序。
147+
\end{itemize}
148+
\chapter{总结与扩展}
149+
拓扑排序是处理依赖关系的核心算法。通过 Kahn 算法和 DFS 算法的对比,可根据实际需求选择实现方式。进一步学习可探索:\par
150+
\begin{enumerate}
151+
\item \textbf{强连通分量}:使用 Tarjan 算法识别图中的环。
152+
\item \textbf{动态拓扑排序}:在频繁增删边的场景下维护排序结果。
153+
\item \textbf{练习题}:LeetCode 207(判断能否完成课程)、310(最小高度树)等。
154+
\end{enumerate}
155+
\chapter{参考资源}
156+
\begin{enumerate}
157+
\item 《算法导论》第 22.4 章「拓扑排序」。
158+
\item VisuAlgo 的可视化工具:https://visualgo.net/zh/graphds。
159+
\end{enumerate}

public/blog/2025-04-02/sha256

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1bfb4d9c39e5ecc3db3bab78e05f3ae8e530d04e680e8dadaeeff0730f5ef5bc

0 commit comments

Comments
 (0)