Java 实现 Kahn 拓扑排序算法(附带完整源码)
一、项目背景详细介绍
在大规模系统和复杂软件工程中,任务之间常常存在依赖关系。如何在保证所有依赖关系满足的前提下,找到一个合理的执行顺序,是任务调度、编译构建、数据处理管道设计等场景中的核心问题之一。有向无环图(DAG)可以直观地描述这种依赖关系:顶点代表任务,边 u->v 表示任务 u 必须在任务 v 之前完成。
拓扑排序(Topological Sort)便是针对 DAG 提供的一种线性排序算法,能够在不破坏依赖关系的情况下,对顶点进行线性排列。Kahn 算法(由 Arthur Kahn 于 1962 年提出)是其中最常用的实现之一,其核心思想是:
-
计算每个顶点的入度(in-degree),即有多少条依赖边指向它;
-
将所有入度为零的顶点加入队列;
-
反复从队列中取出一个顶点,将其加入输出序列,然后删除该顶点及其所有出边,降低相邻顶点的入度;
-
若新的入度为零的顶点出现,则继续加入队列;
-
最终若所有顶点都被输出,则成功得到一个合法的拓扑序列,否则图中存在环,不可拓扑排序。
Kahn 算法时间复杂度为 O(n + m),其中 n 为顶点数,m 为边数,空间复杂度 O(n + m)。算法原理简单,适用于大规模 DAG 的在线或离线拓扑排序。
二、项目需求详细介绍
本项目旨在帮助读者深入理解并实现 Kahn 拓扑排序算法,具体需求如下:
-
输入格式:
-
从控制台读取整数
n(顶点数)和m(边数); -
读取
m行,每行两个整数u v(0 ≤ u,v < n),表示一条有向边u->v。
-
-
功能要求:
-
构建邻接列表(Adjacency List)结构存储图;
-
统计并维护每个顶点的入度值;
-
使用队列实现 Kahn 算法核心流程,生成拓扑排序序列;
-
检测环:若最终输出顶点数量少于
n,则报告“图中存在环,无法完成拓扑排序”; -
输出结果:若成功,打印完整的拓扑序列;若失败,打印错误提示。
-
-
性能与规模:
-
支持
n和m均可达10^5级别的大规模输入; -
保证算法运行时间在可接受范围内(单次排序完成时间 < 1 秒);
-
-
可扩展性:
-
模块化设计,方便后续集成到任务调度器或构建工具中;
-
注释清晰,适合教学与二次开发。
-
三、相关技术详细介绍
-
邻接列表:
-
以
List<List<Integer>> graph存储图,适用于稀疏图(边远少于n^2)场景; -
构建成本 O(n + m),访问和遍历所有出边总计 O(m);
-
-
入度数组:
-
用
int[] inDegree存储每个顶点的当前入度; -
初始化时遍历边集统计入度,成本 O(m);
-
-
队列结构:
-
使用
ArrayDeque或LinkedList实现先进先出队列,支持 O(1) 时间的入队与出队操作;
-
-
时间与空间复杂度:
-
总体时间复杂度 O(n + m),主要耗时在邻接列表构建、入度初始化和 BFS 式遍历;
-
空间复杂度 O(n + m),用于存储邻接列表、入度数组和队列。
-
-
异常与边界处理:
-
输入顶点或边数异常时抛出提示;
-
检测自环或重复边可选,若需要可在构建时过滤或警告。
-
四、实现思路详细介绍
-
输入与数据读取:
-
使用
Scanner读取n与m; -
初始化
graph为new ArrayList<>(n),并为每个i添加空列表; -
读取每条边
u v,调用graph.get(u).add(v),同时inDegree[v]++;
-
-
初始化队列:
-
遍历
0到n-1,将所有inDegree[i] == 0的顶点加入queue;
-
-
核心循环:
-
当
queue非空时:-
u = queue.poll(); -
将
u追加到结果列表List<Integer> topoOrder; -
遍历
graph.get(u)中的每个v:-
inDegree[v]--; -
若
inDegree[v] == 0,则queue.offer(v);
-
-
-
-
结果判断:
-
如果
topoOrder.size() < n,则说明图中存在环,未能输出所有顶点; -
否则,
topoOrder即为合法的拓扑排序序列;
-
-
输出与示例:
-
打印“拓扑排序结果:”和序列;
-
若失败,打印“图中存在环,无法完成拓扑排序”;
-
示例:
输入: 6 6 5 2 5 0 4 0 4 1 2 3 3 1 输出: 拓扑排序结果:[4, 5, 2, 0, 3, 1]
-
五、完整实现源码
import java.util.*;
/**
* Kahn 拓扑排序算法实现
*/
public class KahnTopologicalSort {
/**
* 执行拓扑排序
* @param n 顶点数量
* @param edges 边列表,每个元素为 [u, v]
* @return 拓扑序列列表,如果含环返回空列表
*/
public static List<Integer> topoSort(int n, List<int[]> edges) {
// 构建邻接列表和入度数组
List<List<Integer>> graph = new ArrayList<>(n);
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
int[] inDegree = new int[n];
for (int[] e : edges) {
int u = e[0], v = e[1];
graph.get(u).add(v);
inDegree[v]++;
}
// 初始化队列
Deque<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) queue.offer(i);
}
List<Integer> topoOrder = new ArrayList<>();
// 核心循环
while (!queue.isEmpty()) {
int u = queue.poll();
topoOrder.add(u);
for (int v : graph.get(u)) {
if (--inDegree[v] == 0) {
queue.offer(v);
}
}
}
// 检测环
if (topoOrder.size() < n) {
return Collections.emptyList();
}
return topoOrder;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入顶点数 n 和边数 m:");
int n = scanner.nextInt();
int m = scanner.nextInt();
List<int[]> edges = new ArrayList<>(m);
System.out.println("请输入每条有向边 u v:");
for (int i = 0; i < m; i++) {
edges.add(new int[]{scanner.nextInt(), scanner.nextInt()});
}
scanner.close();
List<Integer> topoOrder = topoSort(n, edges);
if (topoOrder.isEmpty()) {
System.out.println("图中存在环,无法完成拓扑排序。");
} else {
System.out.println("拓扑排序结果:" + topoOrder);
}
}
}
六、代码详细解读(只说明方法作用)
-
topoSort方法:-
构建邻接列表和入度数组;
-
初始化并维护入度为 0 的队列;
-
执行 Kahn 算法核心循环生成拓扑序列;
-
检测环并返回结果;
-
-
main方法:-
读取输入数据;
-
调用
topoSort并输出结果或错误信息;
-
七、项目详细总结
通过本项目,读者可掌握:
-
Kahn 算法原理:基于入度和队列的贪心思想;
-
邻接列表与入度数组结合:高效处理稀疏图;
-
环检测:简单判断完成结果与顶点数量是否一致;
-
模块化设计:可将
topoSort集成到更大系统,如构建工具或任务调度框架。
八、项目常见问题及解答
-
Q:为何返回空列表表示含环?
-
A:当图含环时,算法无法消除所有顶点,
topoOrder.size() < n;
-
-
Q:队列初始化为何包括所有入度为零顶点?
-
A:这些顶点没有依赖,可任意开始执行;
-
-
Q:如何处理顶点标识非整数的情况?
-
A:可将字符或字符串映射到整数索引,再调用算法;
-
-
Q:支持动态添加或删除边吗?
-
A:可在更新
inDegree后重新调用topoSort或设计增量维护结构;
-
九、扩展方向与性能优化
-
在线拓扑维护:支持在图更新时快速调整序列;
-
多源拓扑分支:当队列中多个顶点可选时,支持自定义顺序优先级;
-
并行拓扑排序:将不同入度为零节点并行处理;
-
可视化展示:使用 JavaFX 或 Web 前端图形化演示排序过程;
-
集成依赖管理:将算法封装为库,供构建工具或任务调度系统调用;
165

被折叠的 条评论
为什么被折叠?



