<think>拓扑排序是针对有向无环图(DAG)的一种线性排序算法,使得对于任何有向边(u,v),节点u在排序中出现在节点v之前。以下是使用Java实现拓扑排序的两种方法:Kahn算法(基于入度)和DFS(深度优先搜索)算法。###1.Kahn算法(基于入度)Kahn算法通过不断移除入度为0的节点来完成拓扑排序。```javaimportjava.util.*;publicclassTopologicalSortKahn{publicstaticList<Integer>topologicalSort(intnumVertices,List<List<Integer>>graph){//计算每个节点的入度int[]inDegree=newint[numVertices];for(inti=0;i<numVertices;i++){for(intneighbor:graph.get(i)){inDegree[neighbor]++;}}//将入度为0的节点加入队列Queue<Integer>queue=newLinkedList<>();for(inti=0;i<numVertices;i++){if(inDegree[i]==0){queue.offer(i);}}List<Integer>result=newArrayList<>();intvisitedCount=0;while(!queue.isEmpty()){intnode=queue.poll();result.add(node);visitedCount++;//减少邻居节点的入度for(intneighbor:graph.get(node)){inDegree[neighbor]--;if(inDegree[neighbor]==0){queue.offer(neighbor);}}}//如果图中存在环,则无法完成拓扑排序if(visitedCount!=numVertices){thrownewIllegalStateException("Graphhasacycle");}returnresult;}publicstaticvoidmain(String[]args){//示例:构建有向图(邻接表表示)intnumVertices=6;List<List<Integer>>graph=newArrayList<>();for(inti=0;i<numVertices;i++){graph.add(newArrayList<>());}//添加边graph.get(5).add(2);graph.get(5).add(0);graph.get(4).add(0);graph.get(4).add(1);graph.get(2).add(3);graph.get(3).add(1);//拓扑排序List<Integer>sortedOrder=topologicalSort(numVertices,graph);System.out.println("拓扑排序结果(Kahn算法):"+sortedOrder);}}```**解释**:1.**初始化入度数组**:遍历图的邻接表,计算每个节点的入度。2.**初始化队列**:将所有入度为0的节点加入队列。3.**处理队列**:-从队列中取出节点,加入结果列表。-遍历该节点的所有邻居,将邻居的入度减1。-若邻居的入度变为0,则加入队列。4.**检查环**:如果结果列表中的节点数不等于图中节点总数,说明图中存在环,无法进行拓扑排序。###2.DFS算法(深度优先搜索)使用DFS进行拓扑排序,需要在DFS完成遍历节点后,将节点插入结果列表的头部(逆序)。```javaimportjava.util.*;publicclassTopologicalSortDFS{publicstaticList<Integer>topologicalSort(intnumVertices,List<List<Integer>>graph){boolean[]visited=newboolean[numVertices];Stack<Integer>stack=newStack<>();//对每个未访问节点调用DFSfor(inti=0;i<numVertices;i++){if(!visited[i]){dfs(i,graph,visited,stack);}}//将栈中的元素弹出即为拓扑排序结果List<Integer>result=newArrayList<>();while(!stack.isEmpty()){result.add(stack.pop());}returnresult;}privatestaticvoiddfs(intnode,List<List<Integer>>graph,boolean[]visited,Stack<Integer>stack){visited[node]=true;//遍历所有邻居for(intneighbor:graph.get(node)){if(!visited[neighbor]){dfs(neighbor,graph,visited,stack);}}//当前节点所有邻居处理完后,将当前节点压入栈stack.push(node);}publicstaticvoidmain(String[]args){//构建相同的图intnumVertices=6;List<List<Integer>>graph=newArrayList<>();for(inti=0;i<numVertices;i++){graph.add(newArrayList<>());}graph.get(5).add(2);graph.get(5).add(0);graph.get(4).add(0);graph.get(4).add(1);graph.get(2).add(3);graph.get(3).add(1);//拓扑排序List<Integer>sortedOrder=topologicalSort(numVertices,graph);System.out.println("拓扑排序结果(DFS算法):"+sortedOrder);}}```**解释**:1.**DFS遍历**:对每个未访问节点进行深度优先搜索。2.**后序压栈**:在DFS过程中,当一个节点的所有邻居都被访问后,将该节点压入栈中。3.**结果构建**:完成所有DFS后,栈中元素从栈顶到栈底的顺序即为拓扑排序的逆序,将其弹出即可得到拓扑排序。###两种算法比较|特性|Kahn算法|DFS算法||--------------|------------------------------|-----------------------------||**原理**|基于入度,队列|基于深度优先搜索||**检测环**|通过计数节点是否全部访问|通过递归栈检测环(需额外处理)||**性能**|O(V+E)|O(V+E)||**适用性**|易于理解,需要入度信息|递归实现简洁|###环检测在Kahn算法中,如果结果列表中的节点数少于总节点数,则说明存在环。在DFS算法中,如果递归过程中遇到正在访问的节点(即递归栈中的节点),则说明存在环(但上述DFS实现未显式检测环,需要修改以检测环)。**DFS算法改进(带环检测)**:```javapublicstaticList<Integer>topologicalSortWithCycleDetection(intnumVertices,List<List<Integer>>graph){boolean[]visited=newboolean[numVertices];boolean[]inStack=newboolean[numVertices];//用于检测环Stack<Integer>stack=newStack<>();for(inti=0;i<numVertices;i++){if(!visited[i]){if(dfsWithCycleDetection(i,graph,visited,inStack,stack)){thrownewIllegalStateException("Graphhasacycle");}}}//弹出栈中元素得到拓扑排序List<Integer>result=newArrayList<>();while(!stack.isEmpty()){result.add(stack.pop());}returnresult;}privatestaticbooleandfsWithCycleDetection(intnode,List<List<Integer>>graph,boolean[]visited,boolean[]inStack,Stack<Integer>stack){if(inStack[node]){returntrue;//存在环}if(visited[node]){returnfalse;}visited[node]=true;inStack[node]=true;for(intneighbor:graph.get(node)){if(dfsWithCycleDetection(neighbor,graph,visited,inStack,stack)){returntrue;}}inStack[node]=false;stack.push(node);returnfalse;}```###应用场景-任务调度(依赖关系)-编译顺序(源文件依赖)-课程安排-工作流程管理§§相关问题§§:1.如何检测有向图中是否存在环?2.除了拓扑排序,还有哪些图排序算法?3.拓扑排序在软件工程中有哪些实际应用?4.如何处理带权重的有向无环图(DAG)的拓扑排序?</think># 拓扑排序Java实现详解
拓扑排序是针对有向无环图(DAG)的一种线性排序算法,使得对于任何有向边(u, v),节点u在排序中出现在节点v之前。以下是完整的Java实现:
```java
import java.util.*;
public class TopologicalSort {
// 使用邻接表表示有向图
private Map<String, List<String>> graph = new HashMap<>();
private List<String> vertices = new ArrayList<>();
// 添加顶点
public void addVertex(String vertex) {
if (!graph.containsKey(vertex)) {
graph.put(vertex, new ArrayList<>());
vertices.add(vertex);
}
}
// 添加有向边
public void addEdge(String from, String to) {
addVertex(from);
addVertex(to);
graph.get(from).add(to);
}
// Kahn算法实现拓扑排序(基于入度)
public List<String> topologicalSortKahn() {
// 计算每个顶点的入度
Map<String, Integer> inDegree = new HashMap<>();
for (String vertex : vertices) {
inDegree.put(vertex, 0);
}
for (List<String> edges : graph.values()) {
for (String to : edges) {
inDegree.put(to, inDegree.get(to) + 1);
}
}
// 初始化入度为0的队列
Queue<String> queue = new LinkedList<>();
for (String vertex : vertices) {
if (inDegree.get(vertex) == 0) {
queue.add(vertex);
}
}
// 处理队列
List<String> result = new ArrayList<>();
int count = 0;
while (!queue.isEmpty()) {
String current = queue.poll();
result.add(current);
count++;
// 减少邻居的入度
for (String neighbor : graph.get(current)) {
inDegree.put(neighbor, inDegree.get(neighbor) - 1);
if (inDegree.get(neighbor) == 0) {
queue.add(neighbor);
}
}
}
// 检查是否存在环
if (count != vertices.size()) {
throw new IllegalStateException("Graph has a cycle, topological sort not possible");
}
return result;
}
// DFS实现拓扑排序
public List<String> topologicalSortDFS() {
Stack<String> stack = new Stack<>();
Set<String> visited = new HashSet<>();
Set<String> recursionStack = new HashSet<>();
for (String vertex : vertices) {
if (!visited.contains(vertex)) {
if (!dfs(vertex, visited, recursionStack, stack)) {
throw new IllegalStateException("Graph has a cycle");
}
}
}
List<String> result = new ArrayList<>();
while (!stack.isEmpty()) {
result.add(stack.pop());
}
return result;
}
private boolean dfs(String vertex, Set<String> visited, Set<String> recursionStack, Stack<String> stack) {
visited.add(vertex);
recursionStack.add(vertex);
for (String neighbor : graph.get(vertex)) {
if (!visited.contains(neighbor)) {
if (!dfs(neighbor, visited, recursionStack, stack)) {
return false;
}
} else if (recursionStack.contains(neighbor)) {
return false; // 发现环
}
}
recursionStack.remove(vertex);
stack.push(vertex);
return true;
}
// 获取所有可能的拓扑排序(回溯法)
public List<List<String>> getAllTopologicalOrders() {
List<List<String>> results = new ArrayList<>();
Map<String, Integer> inDegree = new HashMap<>();
// 初始化入度
for (String vertex : vertices) {
inDegree.put(vertex, 0);
}
for (List<String> edges : graph.values()) {
for (String to : edges) {
inDegree.put(to, inDegree.get(to) + 1);
}
}
boolean[] visited = new boolean[vertices.size()];
List<String> currentOrder = new ArrayList<>();
backtrack(inDegree, visited, currentOrder, results);
return results;
}
private void backtrack(Map<String, Integer> inDegree, boolean[] visited,
List<String> currentOrder, List<List<String>> results) {
// 找到所有当前入度为0且未访问的顶点
List<String> candidates = new ArrayList<>();
for (int i = 0; i < vertices.size(); i++) {
String vertex = vertices.get(i);
if (inDegree.get(vertex) == 0 && !visited[i]) {
candidates.add(vertex);
}
}
// 如果没有候选顶点,检查是否完成排序
if (candidates.isEmpty()) {
if (currentOrder.size() == vertices.size()) {
results.add(new ArrayList<>(currentOrder));
}
return;
}
// 尝试每个候选顶点
for (String candidate : candidates) {
int index = vertices.indexOf(candidate);
visited[index] = true;
currentOrder.add(candidate);
// 减少邻居的入度
Map<String, Integer> changes = new HashMap<>();
for (String neighbor : graph.get(candidate)) {
changes.put(neighbor, inDegree.get(neighbor));
inDegree.put(neighbor, inDegree.get(neighbor) - 1);
}
// 递归
backtrack(inDegree, visited, currentOrder, results);
// 回溯:恢复状态
for (Map.Entry<String, Integer> entry : changes.entrySet()) {
inDegree.put(entry.getKey(), entry.getValue());
}
currentOrder.remove(currentOrder.size() - 1);
visited[index] = false;
}
}
// 测试用例
public static void main(String[] args) {
TopologicalSort ts = new TopologicalSort();
// 构建课程依赖图
ts.addEdge("数学基础", "离散数学");
ts.addEdge("离散数学", "数据结构");
ts.addEdge("程序设计", "数据结构");
ts.addEdge("数据结构", "算法分析");
ts.addEdge("程序设计", "面向对象");
ts.addEdge("面向对象", "软件工程");
ts.addEdge("离散数学", "数据库");
ts.addEdge("数据库", "软件工程");
System.out.println("=== Kahn算法拓扑排序 ===");
List<String> kahnResult = ts.topologicalSortKahn();
System.out.println(kahnResult);
System.out.println("\n=== DFS算法拓扑排序 ===");
List<String> dfsResult = ts.topologicalSortDFS();
System.out.println(dfsResult);
System.out.println("\n=== 所有可能的拓扑排序 ===");
List<List<String>> allOrders = ts.getAllTopologicalOrders();
for (int i = 0; i < Math.min(5, allOrders.size()); i++) {
System.out.println("排序 " + (i+1) + ": " + allOrders.get(i));
}
System.out.println("总排序数: " + allOrders.size());
// 测试环检测
System.out.println("\n=== 环检测测试 ===");
ts.addEdge("软件工程", "程序设计"); // 创建环
try {
ts.topologicalSortKahn();
} catch (IllegalStateException e) {
System.out.println("Kahn算法检测到环: " + e.getMessage());
}
try {
ts.topologicalSortDFS();
} catch (IllegalStateException e) {
System.out.println("DFS算法检测到环: " + e.getMessage());
}
}
}
```
## 算法详解
### 1. Kahn算法(基于入度)
```java
public List<String> topologicalSortKahn() {
// 计算入度
Map<String, Integer> inDegree = new HashMap<>();
// ...初始化入度
// 初始化入度为0的队列
Queue<String> queue = new LinkedList<>();
while (!queue.isEmpty()) {
String current = queue.poll();
result.add(current);
// 减少邻居的入度
for (String neighbor : graph.get(current)) {
inDegree.put(neighbor, inDegree.get(neighbor) - 1);
if (inDegree.get(neighbor) == 0) {
queue.add(neighbor);
}
}
}
// 检查环
if (count != vertices.size()) {
throw new IllegalStateException("Graph has a cycle");
}
}
```
**算法步骤**:
1. 计算每个节点的入度(指向该节点的边数)
2. 将所有入度为0的节点加入队列
3. 从队列中取出节点,加入结果列表
4. 将该节点的所有邻居的入度减1
5. 如果邻居入度变为0,加入队列
6. 重复3-5直到队列为空
7. 如果结果节点数 ≠ 总节点数,说明存在环
**时间复杂度**: O(V+E),其中V是顶点数,E是边数
### 2. DFS算法
```java
public List<String> topologicalSortDFS() {
Stack<String> stack = new Stack<>();
Set<String> visited = new HashSet<>();
Set<String> recursionStack = new HashSet<>();
for (String vertex : vertices) {
if (!visited.contains(vertex)) {
if (!dfs(vertex, visited, recursionStack, stack)) {
throw new IllegalStateException("Graph has a cycle");
}
}
}
// 反转栈得到结果
List<String> result = new ArrayList<>();
while (!stack.isEmpty()) {
result.add(stack.pop());
}
return result;
}
private boolean dfs(String vertex, Set<String> visited, Set<String> recursionStack, Stack<String> stack) {
visited.add(vertex);
recursionStack.add(vertex);
for (String neighbor : graph.get(vertex)) {
if (!visited.contains(neighbor)) {
if (!dfs(neighbor, visited, recursionStack, stack)) return false;
} else if (recursionStack.contains(neighbor)) {
return false; // 发现环
}
}
recursionStack.remove(vertex);
stack.push(vertex);
return true;
}
```
**算法步骤**:
1. 使用DFS遍历图
2. 维护递归栈检测环
3. 当节点完成所有邻居访问后,将其压入栈
4. 最终栈中元素从顶到底即为拓扑排序结果(需要反转)
5. 如果发现环立即终止
**特点**:
- 使用递归栈检测环
- 后序遍历完成节点处理
- 结果需要反转
### 3. 所有可能的拓扑排序(回溯法)
```java
public List<List<String>> getAllTopologicalOrders() {
// 初始化入度
Map<String, Integer> inDegree = new HashMap<>();
backtrack(inDegree, visited, currentOrder, results);
return results;
}
private void backtrack(Map<String, Integer> inDegree, boolean[] visited,
List<String> currentOrder, List<List<String>> results) {
// 找到所有当前入度为0且未访问的顶点
List<String> candidates = new ArrayList<>();
// 尝试每个候选顶点
for (String candidate : candidates) {
// 选择候选顶点
// 减少邻居入度
// 递归
// 回溯:恢复状态
}
}
```
**算法思想**:
1. 在每一步选择所有入度为0的候选节点
2. 对于每个候选节点:
- 将其加入当前路径
- 减少其所有邻居的入度
- 递归处理
- 回溯:恢复邻居入度并从路径中移除
3. 当所有节点都加入路径时,保存结果
**应用场景**:
- 任务调度中寻找所有可能的执行顺序
- 课程安排中的多种选择
- 优化并行执行计划
## 拓扑排序的应用场景
1. **任务调度系统**:
- 确定任务执行顺序
- 识别任务依赖关系
- 优化并行执行计划
2. **构建系统**:
- 确定编译顺序(如Makefile)
- 解决文件依赖关系
- 增量构建优化
3. **课程安排**:
- 确定课程选修顺序
- 规划学习路径
- 识别先修课程要求
4. **数据流分析**:
- 确定数据处理顺序
- 优化流水线执行
- 识别循环依赖
5. **电路设计**:
- 确定逻辑门评估顺序
- 优化信号传播路径
- 检测组合逻辑环
## 环检测的重要性
拓扑排序只能应用于有向无环图(DAG)。在实现中,我们通过两种方式检测环:
1. Kahn算法:如果最终排序节点数 < 总节点数,说明存在环
2. DFS算法:通过递归栈检测后向边(back edge)
环检测在以下场景至关重要:
- 避免无限循环的任务调度
- 防止编译过程中的循环依赖
- 确保课程安排不出现不可能满足的先修条件
- 识别数据处理流水线中的死循环