拓扑排序
本文介绍了拓扑排序的定义、应用以及常用算法,并通过LeetCode相关题目进行讲解。
拓扑排序
一、拓扑排序的定义
在计算机科学中,拓扑排序(Topological Sorting)是一种用于有向无环图(DAG, Directed Acyclic Graph)的线性排序方法。它将图中的所有顶点排列成一个线性序列,使得对于每一条有向边 (u, v),顶点 u 在序列中出现在顶点 v 之前。拓扑排序在许多实际问题中都有广泛的应用,如任务调度、编译顺序、课程安排等。
二、拓扑排序的应用
拓扑排序在许多实际问题中都有广泛的应用,主要包括但不限于以下几个方面:
- 任务调度:在项目管理中,任务之间可能存在依赖关系。拓扑排序可以帮助确定任务的执行顺序,确保所有依赖关系得到满足。
- 编译顺序:在软件开发中,源代码文件之间可能存在依赖关系。拓扑排序可以确定编译这些文件的正确顺序。
- 课程安排:在教育领域,某些课程可能有先修课程的要求。拓扑排序可以帮助学生规划课程学习顺序。
- 数据处理流水线:在数据处理过程中,某些步骤可能依赖于前面的步骤。拓扑排序可以帮助确定数据处理的顺序。
- 版本控制系统:在版本控制中,提交之间可能存在依赖关系。拓扑排序可以帮助确定合并和发布的顺序。
三、常用算法
3.1 Kahn算法
Kahn算法是一种基于入度的拓扑排序方法。其基本思想是:
- 计算图中每个顶点的入度。
- 将所有入度为0的顶点加入一个队列。
- 重复以下步骤直到队列为空:
- 从队列中取出一个顶点,将其加入拓扑排序结果中。
- 对该顶点的每个邻接点,将其入度减1。如果邻接点的入度变为0,则将其加入队列。
- 如果拓扑排序结果中包含所有顶点,则排序成功;否则,图中存在环,无法进行拓扑排序。
3.2 深度优先搜索(DFS)算法
DFS算法通过深度优先遍历图来实现拓扑排序。其基本思想是:
- 初始化一个空的栈和一个访问标记数组。
- 对图中的每个顶点执行以下操作:
- 如果该顶点未被访问,执行DFS遍历。
- 在DFS过程中,访问每个顶点的邻接点,递归进行DFS。
- 当一个顶点的所有邻接点都被访问后,将该顶点压入栈中。
- 最后,从栈中依次弹出顶点,得到拓扑排序结果。
- 如果在DFS过程中发现有顶点被重复访问,说明图中存在环,无法进行拓扑排序。
四、LeetCode 相关题目
1. 例一:leetcode 310. 最小高度树
1.1 题目描述
给定一个无向图,找到所有的最小高度树的根节点。
1.2 解题思路
- 构建图的邻接表表示。
- 计算每个节点的度数。
- 使用类似Kahn算法的思路,从所有度数为1的叶子节点开始,逐层剥离叶子节点,直到剩下1或2个节点,这些节点即为最小高度树的根节点。
- 返回剩下的节点列表。
1.3 算法正确性证明
通过逐层剥离叶子节点,最终剩下的节点必然是距离图中心最近的节点,这些节点作为根节点可以形成最小高度树。
1.4 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution {
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
if (n == 1) return Collections.singletonList(0);
// 使用List<Integer>[] 提高效率
List<Integer>[] adj = new List[n];
for (int i = 0; i < n; i++) {
adj[i] = new ArrayList<>();
}
int[] degree = new int[n];
for (int[] edge : edges) {
int u = edge[0], v = edge[1];
adj[u].add(v);
adj[v].add(u);
degree[u]++;
degree[v]++;
}
// 叶子节点队列
Queue<Integer> leaves = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (degree[i] == 1) {
leaves.offer(i);
}
}
int remainingNodes = n;
while (remainingNodes > 2) {
int size = leaves.size();
remainingNodes -= size;
for (int i = 0; i < size; i++) {
int leaf = leaves.poll();
// 处理所有邻居
for (int neighbor : adj[leaf]) {
degree[neighbor]--;
if (degree[neighbor] == 1) {
leaves.offer(neighbor);
}
}
}
}
return new ArrayList<>(leaves);
}
}
2. 例二:leetcode 207. 课程表
2.1 题目描述
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1] 。给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
2.2 解题思路
- 构建图的邻接表表示。
- 计算每个节点的入度。
- 使用Kahn算法的思路,从所有入度为0的节点开始,逐层删除节点,并更新邻接点的入度。
- 如果最终删除的节点数量等于课程总数,则表示可以完成所有课程;否则,存在环,无法完成所有课程。
- 返回布尔值结果。
2.3 算法正确性证明
通过Kahn算法的拓扑排序过程,如果能够删除所有节点,说明图中不存在环,课程可以按顺序完成。
2.4 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
//邻接表
List<List<Integer>> graph = new ArrayList<>();
for(int i=0;i<numCourses;i++){
graph.add(new ArrayList<>());
}
//入度数组
int[] indegree = new int[numCourses];
//遍历边,构建图
for(int[] entry : prerequisites){
int from=entry[1];
int to=entry[0];
graph.get(from).add(to);
indegree[to]++;
}
//构建队列,将所有入度为0的点加入队列
Queue<Integer> queue = new LinkedList<>();
for(int i=0;i<numCourses;i++){
if(indegree[i]==0) queue.add(i);
}
//独立的课程,也就是在构建拓扑排序的过程中,入度和出度都为0的节点
int count=0;
while(!queue.isEmpty()){
int course=queue.poll();
count++;
for(int neighbor : graph.get(course)){
indegree[neighbor]--;
if(indegree[neighbor]==0) queue.add(neighbor);
}
}
return count==numCourses;
}
}
本文由作者按照 CC BY 4.0 进行授权