拓扑排序(Kahn算法)
什么是拓扑排序
拓扑排序是对 有向无环图(DAG, Directed Acyclic Graph) 的顶点进行的一种线性排序,使得对于每一条有向边 (u, v),顶点 u 在排序结果中都出现在顶点 v 的前面。
核心前提:图必须是 DAG(无环),若图中存在环,则无法进行拓扑排序。
应用场景
拓扑排序主要用于处理依赖关系的场景,典型例子包括:
- 课程安排:大学选课系统中,某些课程有先修课要求(如 "数据结构" 必须在 "算法分析" 之前学习),拓扑排序可给出合理的选课顺序。
- 任务调度:项目管理中,多个任务存在依赖关系(如 "设计" 完成后才能 "开发"),拓扑排序可生成任务执行序列。
- 编译顺序:编译器处理文件依赖时(如 .h 头文件被 .cpp 源文件依赖),拓扑排序可确定文件的编译顺序。
三、Kahn 算法原理与步骤
Kahn 算法是一种基于入度的贪心算法,核心思想是 "不断处理入度为 0 的顶点"。
原理
- 入度定义:顶点的入度是指指向该顶点的有向边数量。
- 贪心策略:每次选择入度为 0 的顶点(无前置依赖),将其加入拓扑序列,然后 "移除" 该顶点的所有出边(即减少邻接顶点的入度)。
环检测:若最终拓扑序列的顶点数小于图的总顶点数,说明图中存在环。
步骤
- 初始化:
计算图中所有顶点的入度,存入 inDegree 数组。
使用一个队列(或栈)存储所有入度为 0 的顶点。 - 循环处理:
从队列中取出一个顶点 u,加入拓扑序列。
遍历 u 的所有邻接顶点 v,将 v 的入度减 1。
若 v 的入度变为 0,将其加入队列。 - 终止条件:
队列为空时,检查拓扑序列长度:
若长度等于顶点数:输出拓扑序列。
若长度小于顶点数:图中存在环,无法拓扑排序。
java
import java.util.*;
public class TopologicalSortKahn {
// 拓扑排序函数,返回拓扑序列(若有环则返回空列表)
public static List<Integer> topologicalSort(int numVertices, List<List<Integer>> adj) {
int[] inDegree = new int[numVertices]; // 存储每个顶点的入度
List<Integer> topoOrder = new ArrayList<>(); // 存储拓扑序列
// 1. 计算所有顶点的入度
for (int u = 0; u < numVertices; u++) {
for (int v : adj.get(u)) {
inDegree[v]++; // 边 u->v,v 的入度+1
}
}
// 2. 初始化队列,加入所有入度为 0 的顶点
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numVertices; i++) {
if (inDegree[i] == 0) {
queue.offer(i);
}
}
// 3. 循环处理队列中的顶点
while (!queue.isEmpty()) {
int u = queue.poll(); // 取出入度为 0 的顶点 u
topoOrder.add(u); // 加入拓扑序列
// 遍历 u 的所有邻接顶点 v
for (int v : adj.get(u)) {
inDegree[v]--; // 移除边 u->v,v 的入度-1
if (inDegree[v] == 0) {
queue.offer(v); // 若 v 入度为 0,加入队列
}
}
}
// 4. 检测是否有环:若拓扑序列长度不等于顶点数,说明有环
if (topoOrder.size() != numVertices) {
return new ArrayList<>(); // 有环,返回空列表
}
return topoOrder;
}
// 测试主函数
public static void main(String[] args) {
// 示例:构建一个 DAG(顶点 0~5)
int numVertices = 6;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numVertices; i++) {
adj.add(new ArrayList<>());
}
// 添加边:u->v(u 是 v 的前置依赖)
adj.get(0).add(1); // 0->1
adj.get(0).add(2); // 0->2
adj.get(1).add(3); // 1->3
adj.get(2).add(3); // 2->3
adj.get(3).add(4); // 3->4
adj.get(3).add(5); // 3->5
// 执行拓扑排序
List<Integer> result = topologicalSort(numVertices, adj);
// 输出结果
if (result.isEmpty()) {
System.out.println("图中存在环,无法拓扑排序!");
} else {
System.out.println("拓扑排序结果:" + result);
}
}
}