Java数据结构与算法——拓扑排序

news/2025/3/30 2:47:08/

拓扑排序

概念

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

拓扑排序主要用来解决有向图中的依赖解析(dependency resolution)问题。

举例来说,如果我们将一系列需要运行的任务构成一个有向图,图中的有向边则代表某一任务必须在另一个任务之前完成这一限制。那么运用拓扑排序,我们就能得到满足执行顺序限制条件的一系列任务所需执行的先后顺序。当然也有可能图中并不存在这样一个拓扑顺序,这种情况下我们无法根据给定要求完成这一系列任务,这种情况称为循环依赖(circular dependency)。

例1:如下图所示为一个有向图:

在这里插入图片描述

可以得到两个不同的拓扑排序结果:[1, 2, 3, 4, 5][1, 2, 3, 5, 4]

拓扑排序存在的前提:

当且仅当一个有向图为有向无环图(directed acyclic graph,或称DAG)时,才能得到对应于该图的拓扑排序。每一个有向无环图都至少存在一种拓扑排序。该论断可以利用反证法证明如下:

假设我们有一由gif.latex?v_1gif.latex?v_n这n个结点构成的有向图,且图中gif.latex?v_1%2C%20v_2%2C...%2Cv_n这些结点构成一个环。这即是说对于所有1≤i<n-1,图中存在一条有向边从gif.latex?v_i指向gif.latex?v_%7Bi+1%7D。同时还存在一条从gif.latex?v_n指向gif.latex?v_1的边。假设该图存在一个拓扑排序。

那么基于这样一个有向图,显然我们可以得知对于所有1≤i<n-1,必须在之前被遍历,也就是必须在之前被遍历。同时由于还存在一条从指向的边,必须在之前被遍历。这里出现了与我们的假设所冲突的结果。因此我们可以知道,该图存在拓扑排序的假设不成立。也就是说,对于非有向无环图而言,其拓扑排序不存在。

实现方式

bfs广度遍历

与普通的广度优先遍历唯一的区别在于需要维护每一个节点对应的入度,并在遍历的每一层时选取入度为0的节点开始遍历(而普通的广度优先遍历则无此限制,可以从每一层任意一个节点开始遍历)。这个算法描述如下:

  1. 统计图的每一个节点的入度存储与数组inDegree。
  2. 选取入度为0的节点加入队列
  3. 从队列中取出一个节点,
    (a) 将该节点加入输出
    (b) 将该节点的所有邻接点的入度树减1,减1后入度数变为0的节点加入队列
  4. 重复步骤3,直到遍历完所有的结点。
  5. 如果无法遍历完所有的结点,则意味着当前的图不是有向无环图。不存在拓扑排序。

**由于只有入度为0的节点才会被放入队列,当存在环时,环上的节点将不会放入队列,因此不会出现在最终的拓扑排序中。**事实上,在基于广度优先搜索的拓扑排序中,可以根据最终拓扑排序输出列表的长度是否等于图的节点数,来判断输入图是否存在拓扑排序。

时间复杂度: gif.latex?O%28n%20+%20e%29,其中n为图中的结点数目,e为图中的边的数目

空间复杂度: gif.latex?O%28n%20+%20e%29,需要存储成邻接表,空间复杂度为gif.latex?O%28n%20+%20e%29

leetcode题:

import java.util.LinkedList;
import java.util.Queue;/*** Description:leetcode-207. 课程表** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/3*/
public class Main207 {/**** 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。* 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,* 表示如果要学习课程 ai 则 必须 先学习课程  bi 。** 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。* 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。** 输入:numCourses = 2, prerequisites = [[1,0]]* 输出:true* 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。** [[0,2],[1,2],[2,0]] false**//*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点的入度*/int[] inDegree;/*** bfs实现* @param numCourses 节点数* @param prerequisites 依赖关系* @return*/public boolean canFinish(int numCourses, int[][] prerequisites) {// 初始化graph = new LinkedList[numCourses];inDegree = new int[numCourses];for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);inDegree[x] ++;}// 入度0添加Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; ++i) {if (inDegree[i] == 0) {queue.offer(i);}}// 拓扑排序遍历int visited = 0;while (!queue.isEmpty()){visited ++;Integer point = queue.poll();// 指向的节点入度 - 1LinkedList<Integer> edge = graph[point];for (Integer nextPoint : edge) {inDegree[nextPoint] --;// 入度0添加到队列if (inDegree[nextPoint] == 0){queue.offer(nextPoint);}}}return visited == numCourses;}public static void main(String[] args) {Main207 main207 = new Main207();int[][] arr = {{1,0}, {1,2}, {0, 1}};System.out.println(main207.canFinish(3, arr));}
}
import java.util.LinkedList;
import java.util.Queue;/*** Description:leetcode-210. 课程表 II** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/4*/
public class Main210 {/*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点的入度*/int[] inDegree;/*** bfs* @param numCourses 节点数* @param prerequisites 边,依赖关系* @return*/public int[] findOrder(int numCourses, int[][] prerequisites) {int[] result = new int[numCourses];// 初始化graph = new LinkedList[numCourses];inDegree = new int[numCourses];for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);inDegree[x] ++;}// 入度0添加Queue<Integer> queue = new LinkedList<>();for (int i = 0; i < numCourses; ++i) {if (inDegree[i] == 0) {queue.offer(i);}}// 拓扑排序遍历int visited = 0;while (!queue.isEmpty()){Integer point = queue.poll();// 指向的节点入度 - 1LinkedList<Integer> edge = graph[point];for (Integer nextPoint : edge) {// 入度0添加到队列if (--inDegree[nextPoint] == 0){queue.offer(nextPoint);}}result[visited ++] = point;}if (visited != numCourses){return new int[0];}return result;}public static void main(String[] args) {Main210 main210 = new Main210();int[][] arr = {{1,0}};System.out.println(main210.findOrder( 2, arr));}
}

dfs深度遍历

使用深度优先搜索实现拓扑排序的基本思想是:对于一个特定节点,如果该节点的所有相邻节点都已经搜索完成,则该节点也会变成已经搜索完成的节点,在拓扑排序中,该节点位于其所有相邻节点的前面。一个节点的相邻节点指的是从该节点出发通过一条有向边可以到达的节点。

由于拓扑排序的顺序和搜索完成的顺序相反,因此需要使用一个栈存储所有已经搜索完成的节点。深度优先搜索的过程中需要维护每个节点的状态,每个节点的状态可能有三种情况:

  • 0:未访问;
  • 1:访问中;
  • 2:已访问;

初始时,所有节点的状态都是「未访问」。

每一轮搜索时,任意选取一个「未访问」的节点 u,从节点 u 开始深度优先搜索。将节点 u 的状态更新为「访问中」,对于每个与节点 u 相邻的节点 v,判断节点 v 的状态,执行如下操作:

  1. 如果节点 v 的状态是「未访问」,则继续搜索节点 v;
  2. 如果节点 v 的状态是「访问中」,则找到有向图中的环,因此不存在拓扑排序;
  3. 如果节点 v 的状态是「已访问」,则节点 v 已经搜索完成并加入输出排序列表,节点 u 尚未完成搜索,因此节点 u 的拓扑顺序一定在节点 v 的前面,不需要执行任何操作。
  4. 当节点 u 的所有相邻节点的状态都是「已访问」时,将节点 u 的状态更新为「已访问」,并将节点 u 加入输出排序列表。
  5. 当所有节点都访问结束之后,如果没有找到有向图中的环,则存在拓扑排序,所有节点从栈顶到栈底的顺序即为拓扑排序。

时间复杂度: gif.latex?O%28n%20+%20e%29,其中n为图中的结点数目,e为图中的边的数目

空间复杂度: gif.latex?O%28n%20+%20e%29,对图进行深度优先搜索,我们需要存储成邻接表的形式,空间复杂度为gif.latex?O%28n%20+%20e%29,深度优先搜索的过程中,我们需要最多 O(n)的栈空间(递归)进行深度优先搜索,因此总空间复杂度为gif.latex?O%28n%20+%20e%29

leetcode题:

import java.util.LinkedList;
import java.util.Queue;/*** Description:leetcode-207. 课程表** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/3*/
public class Main207 {/**** 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。* 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,* 表示如果要学习课程 ai 则 必须 先学习课程  bi 。** 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。* 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。** 输入:numCourses = 2, prerequisites = [[1,0]]* 输出:true* 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。** [[0,2],[1,2],[2,0]] false**//*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点访问状态, 0未访问 1访问中 2已访问*/int[] visited;/*** 是否存在环*/boolean isCircle = false;/*** dfs实现* @param numCourses 节点数* @param prerequisites 依赖关系* @return*/public boolean canFinish(int numCourses, int[][] prerequisites) {// 初始化graph = new LinkedList[numCourses];visited = new int[numCourses];for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();visited[i] = 0;}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);}// 未访问的节点一直递归for (int i = 0; i < numCourses && !isCircle; i++) {if (visited[i] == 0) {dfs(i);}}return !isCircle;}private void dfs(int point){visited[point] = 1;// 递归指向的相邻节点for (int nextPoint: graph[point]) {if (visited[nextPoint] == 0) {// 未访问则继续向下递归dfs(nextPoint);if (isCircle) {return;}} else if (visited[nextPoint] == 1) {// 出现环isCircle = true;return;}}// 已访问visited[point] = 2;}public static void main(String[] args) {Main207 main207 = new Main207();int[][] arr = {{1,0}, {1,2}, {0, 1}};System.out.println(main207.canFinish( 3, arr));}
}
import java.util.LinkedList;/*** Description:leetcode-210. 课程表 II** 图、拓扑排序** @author wzq* @version 1.0.0* @date 2023/4/4*/
public class Main210 {/*** 邻接表,记录指向的节点*/LinkedList<Integer> graph[];/*** 记录节点访问状态, 0未访问 1访问中 2已访问*/int[] visited;/*** 是否存在环*/boolean isCircle = false;/*** 拓扑排序集*/int[] result;/*** 拓扑排序集下标*/int index;/*** dfs* @param numCourses 节点数* @param prerequisites 边,依赖关系* @return*/public int[] findOrder(int numCourses, int[][] prerequisites) {result = new int[numCourses];// 初始化graph = new LinkedList[numCourses];visited = new int[numCourses];index = numCourses - 1;for (int i = 0; i < numCourses; i++) {graph[i] = new LinkedList<>();}// 先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1int row = prerequisites.length;for (int i = 0; i < row; i++) {int x = prerequisites[i][0];int y = prerequisites[i][1];if (graph[y].contains(x)){continue;}// x依赖y, y指向xgraph[y].add(x);}// 未访问的节点一直递归for (int i = 0; i < numCourses && !isCircle; i++) {if (visited[i] == 0){dfs(i);}}if (isCircle){return new int[0];}return result;}private void dfs(int point){visited[point] = 1;// 递归指向的相邻节点for (Integer nextPoint : graph[point]) {if (visited[nextPoint] == 0){// 未访问则继续向下递归dfs(nextPoint);if (isCircle) {return;}continue;}if(visited[nextPoint] == 1){// 出现环isCircle = true;return;}}// 已访问,由于被依赖的节点先遍历,记录在尾部result[index --] = point;visited[point] = 2;}public static void main(String[] args) {Main210 main210 = new Main210();int[][] arr = {{1,0}};System.out.println(main210.findOrder( 2, arr));}
}

http://www.ppmy.cn/news/39627.html

相关文章

设计模式-单例模式-懒汉式饿汉式探讨

文章目录基础概念饿汉式实例懒汉式实例懒汉式实例【互斥锁方式保障线程安全】懒汉式实例【双重检查锁定&#xff08;Double-Checked Locking&#xff09;保障线程安全】大型项目中单例模式实用数据库连接池C语言-单例模式实现线程池C语言单例模式-实现日志管理器C语言单例模式-…

GP03丨宽窄基资金管理增强策略

量化策略开发&#xff0c;高质量社群&#xff0c;交易思路分享等相关内容 正 文 大家好&#xff0c;今天我们分享股票社群第3期量化策略——ETF资金管理增强策略。 在上一期中&#xff0c;我们分享了基础版ETF轮动&#xff08;结合股票多因子排序逻辑&#xff09;策略&#xf…

免费好用的oa系统有哪些?盘点这几款!

免费好用的oa系统有哪些&#xff1f;盘点这几款&#xff01; 办公自动化&#xff08;OA&#xff09;&#xff0c;英文Office Automation的缩写。它可以通过特定流程或特定环节与日常事务联系在一起&#xff0c;使公文在流转、审批、发布等方面提高效率&#xff0c;实现办公管理…

C++从0到1实战

持续更新中… 1、Windows开发环境的准备 2、第一个C程序 3、C中输出数据 4、C中程序的注释 5、C中变量使用 6、C中常量使用 7、C中标识符的命名 8、C中数据输入 9、C中算术运算 10、C中自增和自减 11、C中赋值运算 12、C11初始化赋值 13、C中关系运算 14、C中逻辑运算 15、C中运…

百度研发效能从度量到数字化蜕变之路

y研发 作者 | 乌拉 导读 企业降本增效的诉求越发强烈&#xff0c;研发效能成为近来极为火爆的话题&#xff0c;本文从效能分析整体思路、实践案例、技术实现介绍了如何从效能度量逐步演变形成基于价值的数字化决策系统的过程&#xff0c;通过本文可以了解到&#xff1a; 1、研…

Android 手机自动化测试工具有哪几种?

一、Android手机自动化测试工具&#xff0c;常用的有这7中&#xff1a; 1、首推Appium&#xff1a; 推荐理由&#xff1a;功能非常强大的移动端自动化测试框架&#xff0c;还免费 下载链接&#xff1a;Appium: Mobile App Automation Made Awesome. Appium是一种被广泛使用的…

数据结构之(三):队列

队列初体验之数组实现方式队列 1、初识队列 队列&#xff0c;是一种对存取有严格要求的数据结构 只能从尾部存入数据&#xff0c;从头部取出数据 遵循“先进先出”原则 队列的实现有两种方式&#xff1a; 顺序队列&#xff08;基于数组&#xff09;、链队列&#xff08;基于链表…

Maven项目中的依赖出现版本冲突,最终发现是对Dependency Scope理解有误

再来个文章目录 文章目录背景疑问排查过程问题存在的原因总结示例依赖版本说明本文记录一下遇到maven依赖版本冲突后的排查过程说明以及问题原因说明 下面还有投票&#xff0c;帮忙投个票&#x1f44d; 背景 最近加入了 Apache Dubbo 开源社区&#xff0c;成为了一名Dubbo Con…