首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

拓扑排序两种实现的代码示例

有些算法我们可能会写,也用过,但就是不记得关于它的“专业术语”了。

比如昨天有网友留言:拓扑排序...

看到这个词我就慌了,这是啥玩意?可不能让网友知道我不知道他说的啥。

今天就通过两个示例来理解这个术语:拓扑排序。

一、拓扑排序的核心概念

拓扑排序(Topological Sorting)是一种对有向无环图(DAG, Directed Acyclic Graph)节点进行排序的算法。

它的目标是将图中的节点排列成一个线性顺序,使得对于每条有向边 (u, v),节点 u 都排在节点 v 的前面。这种排序广泛应用于任务调度、编译依赖分析等场景。

拓扑排序的前提是图中没有环,即图是一个有向无环图。如果图中存在环,那么就无法对节点进行拓扑排序,因为环中的节点无法满足先后顺序的要求。

拓扑排序的输出通常是一个线性序列,表示节点的一个合法的处理顺序。对于一个有多个依赖关系的任务集合,拓扑排序可以帮助确定一个可行的执行顺序。

关键词:有向、无环、图。

比如下面这个图:

在这个图中,节点 1 指向节点 2 和节点 3。

节点 2 和节点 3 都指向节点 4。

节点 4 没有指向其他节点。

节点1的入度为0,也就是说1前面没有节点了。节点2和3的入度为1。节点4的入度为2。

二、拓扑排序的实现方法

拓扑排序有两种主要的实现方法:Kahn 算法(基于入度)和深度优先搜索(DFS)。

1、基于入度的 Kahn 算法

Kahn 算法的核心思想是使用入度(indegree),即每个节点的入边数量。我们将入度为 0 的节点放入队列中,因为它们没有依赖关系,可以首先处理。

算法步骤如下:

计算图中每个节点的入度。

将所有入度为 0 的节点加入队列。

当队列不为空时,取出队列中的节点,将其加入拓扑排序结果中。

遍历该节点的所有邻居节点,将它们的入度减 1,如果某个邻居节点的入度变为 0,则将其加入队列。

重复步骤 3 和 4,直到队列为空。

如果拓扑排序结果中的节点数量等于图中的节点数量,说明排序成功;否则,图中存在环。

下面是 Kahn 算法的 Java 代码实现:

import java.util.*;

/**

* Kahn 算法

*

*/

public class TopologicalSort {

public static List<Integer> kahnTopologicalSort(Map<Integer, List<Integer>> graph, int numNodes) {

List<Integer> result = new ArrayList<>();

// 1、记录每个节点的入度

int[] indegree = new int[numNodes + 1];

// 2、计算每个节点的入度

for (List<Integer> neighbors : graph.values()) {

for (int neighbor : neighbors) {

indegree[neighbor]++;

}

}

// 3、将所有入度为 0 的节点加入队列

Queue<Integer> queue = new LinkedList<>();

for (int i = 1; i <= numNodes; i++) {

if (indegree[i] == 0) {

queue.offer(i);

}

}

// 5、执行 Kahn 算法

while (!queue.isEmpty()) {

int node = queue.poll();

result.add(node);

for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {

// 4、将它们的入度减 1,如果某个邻居节点的入度变为 0,则将其加入队列

indegree[neighbor]--;

if (indegree[neighbor] == 0) {

queue.offer(neighbor);

}

}

}

// 6、检查是否存在环

if (result.size() != numNodes) {

throw new IllegalStateException("图中存在环,无法进行拓扑排序!");

}

return result;

}

public static void main(String[] args) {

// 示例图:邻接列表表示

Map<Integer, List<Integer>> graph = new HashMap<>();

graph.put(1, Arrays.asList(2, 3));

graph.put(2, Arrays.asList(4));

graph.put(3, Arrays.asList(4));

graph.put(4, Arrays.asList());

// 图的顶点数量

int numNodes = 4;

List<Integer> topologicalOrder = kahnTopologicalSort(graph, numNodes);

System.out.println("拓扑排序顺序: " + topologicalOrder);

}

}

在这个代码中:

indegree 数组记录每个节点的入度。

queue 用于存储入度为 0 的节点,按顺序处理。

如果最终 result 的长度不等于节点数量,说明图中有环,无法进行拓扑排序。

运行结果:

拓扑排序顺序: [1, 2, 3, 4]

2、基于 DFS 的拓扑排序

使用 DFS 实现拓扑排序的思路是:对每个节点进行 DFS 遍历,在递归完成时将节点加入栈中。最终栈中的节点顺序即为拓扑排序的顺序。

算法步骤如下:

对每个未访问的节点进行 DFS。

在访问节点的所有邻居节点后,将该节点加入栈。

最终,栈的逆序即为拓扑排序的结果。

下面是 DFS 实现的 Java 代码:

import java.util.*;

/**

* DFS

*/

public class TopologicalSortDFS {

public static void dfs(int node, Map<Integer, List<Integer>> graph, Set<Integer> visited, Stack<Integer> stack) {

// 标记节点为已访问

visited.add(node);

for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {

// 不包含的节点

if (!visited.contains(neighbor)) {

// 循环访问

dfs(neighbor, graph, visited, stack);

}

}

// 2、所有邻居访问完后,将节点加入栈

stack.push(node);

}

public static List<Integer> topologicalSortDFS(Map<Integer, List<Integer>> graph, int numNodes) {

// 已访问集合

Set<Integer> visited = new HashSet<>();

Stack<Integer> stack = new Stack<>();

for (int i = 1; i <= numNodes; i++) {

// 1、对每个未访问的节点进行 DFS

if (!visited.contains(i)) {

dfs(i, graph, visited, stack);

}

}

// 排序结果集合

List<Integer> result = new ArrayList<>();

while (!stack.isEmpty()) {

// 栈逆序即为拓扑排序结果

result.add(stack.pop());

}

// 3、返回排序后的结果集

return result;

}

public static void main(String[] args) {

// 示例图:邻接列表表示

Map<Integer, List<Integer>> graph = new HashMap<>();

graph.put(1, Arrays.asList(2, 3));

graph.put(2, Arrays.asList(4));

graph.put(3, Arrays.asList(4));

graph.put(4, Arrays.asList());

// 图的顶点数量

int numNodes = 4;

List<Integer> topologicalOrder = topologicalSortDFS(graph, numNodes);

System.out.println("拓扑排序顺序: " + topologicalOrder);

}

}

在这个代码中:

visited 集合用于记录已访问的节点,避免重复访问。

stack 存储节点,最终逆序输出得到拓扑排序。

运行结果:

拓扑排序顺序: [1, 3, 2, 4]

Kahn 算法使用LinkedList的队列实现,DFS使用Stack栈实现,队列是先进先出,栈是后进先出,需要注意的是,它俩排序的结果是不一样的,这个还需要看业务场景。

三、拓扑排序的应用场景

拓扑排序在很多实际场景中都很有用,尤其是在任务调度和依赖管理方面:

任务调度:在项目管理中,如果任务之间有先后依赖关系(例如任务 B 依赖于任务 A 完成),可以使用拓扑排序找到任务的执行顺序。

编译依赖管理:在编译大型软件时,源代码文件通常有依赖关系。拓扑排序可以帮助确定文件的编译顺序。

课程安排:在学术课程安排中,某些课程需要先修其他课程,拓扑排序可以用于生成课程的学习顺序。

四、拓扑排序的时间复杂度和空间复杂度

假设图的节点数为 V,边数为 E,则:

时间复杂度:O(V + E),因为每个节点和边最多访问一次。

空间复杂度:O(V),因为需要存储入度数组(Kahn 算法)或递归栈和 visited 集合(DFS)。

五、拓扑排序的注意事项

拓扑排序只适用于有向无环图。如果图中有环,则无法进行拓扑排序。

拓扑排序的结果可能不唯一,如果图中存在多个顺序都满足要求,拓扑排序结果也会不同。

六、最后总结

拓扑排序是一种用于对有向无环图节点进行线性排序的算法,常用于解决依赖问题。它可以通过 Kahn 算法(基于入度)或 DFS 实现,具有 O(V + E) 的时间复杂度,是解决依赖排序问题的高效方法。

真好,又把忘记的算法捡起来了。会写算法,术语也能说个一二,才能一本正经地装X,哈哈。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OT3eTjLLQjqewP4T5DVz_V6g0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券