详细介绍了图的拓扑排序的概念,然后介绍了求拓扑序列的算法:Kahn算法的原理,最后提供了基于邻接矩阵和邻接表的图对该算法的Java实现。

阅读本文需要一定的图的基础,如果对于图不是太明白的可以看看这篇文章:图的入门概念以及存储结构、遍历方式介绍和Java代码的实现。

文章目录

  • 1 拓扑排序的概述
  • 2 Kahn算法
    • 2.1 原理
    • 2.2 案例分析
  • 3 邻接矩阵有向图实现
  • 4 邻接表有向图实现

1 拓扑排序的概述

在图论中,如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图(Directed Acyclic Graph),简称DAG图。

上图中从左到右依次是有向图、有向无环图、树。三者的概念是包含关系:有向图包含有向无环图包含树。

在生活中,图形结构的应用是最广泛的。比如有向图,被大量的运用在项目工程活动流程安排中,因为这些活动一般都是有先后顺序的。在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网(ActivityOn Vertex Network)。

AOV网中的弧表示活动之间存在的某种制约关系,并且不应该存在回路,因为若带有回路,则回路上的所有活动都无法进行。

设G=(V,E)是一个具有n个顶点的有向无环图,V中的顶点序列v1,v2,……,vn,满足若从顶点vi到vj有一条路径,且在顶点序列中顶点vi必在顶点vj之前,每个定点只能出现一次。这样的线性顶点序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。

所谓拓扑排序(Topological Sort),其实就是对一个有向图无环图构造拓扑序列的过程。从离散数学的角度来看,拓扑排序就是由某集合上的一个偏序得到该集合上的一个全序。偏序指集合中仅有部分成员之间可比较(集合存在部分排序关系,但是任然存在某些元素间无法比较),而全序指集合中全体成员之间都可以比较(对于集合中的任何一对元素,在某个关系下都是相互可比较的)。

偏序就像是一个流程图,其中有些步骤是没有明确先后关系的,比如上面的中间的有向无环图中,D和F是无法比较的(无法得知先后顺序),甚至左边路径C-D-B和右边路径的F-G的先后顺序都是无法比较的。拓扑排序的任务是在这个偏序上得到一个全序,即得到一个完成整个项目的各步骤的序列。

正是由于某些步骤间没有规定优先关系(这就是偏序的特点),拓扑排序得到的序列有可能不是唯一的,在实际生活中,比如醒来-穿衣服-穿裤子-出门。醒来一定是最先的,出门一定是最后的,但是穿衣服和穿裤子他们的顺序是可以交换的。在拓扑排序的时候常常需要人为的加入一些规则,使得到的序列为满足偏序关系的一个全序。

假设你正在规划一个项目,并有该项目是一个很大的有向无环图,其中充斥着需要做的事情,但却不知道要从哪里开始。这时就可使用拓扑排序并且根据人为规定的一些先后顺序来创建一个有序的任务列表,让所有的活动都具有先后次序,方便项目的开展。

拓扑排序的常见实现算法是Kahn算法。

2 Kahn算法

2.1 原理

Kahn算法的基本思想是:

  1. 找到入度为0 的顶点找到并记录到队列或者栈中;
  2. 移除找到的入度为0的顶点和对应的以该顶点为起点的边,并将被移除的顶点加入到list集合中,同时移除的顶点作为起点的边的终点的如度减去1;继续循环1的步骤,直至队列或者栈为空。
  3. 此时list集合中的顶点的顺序输出就是拓扑排序的结果;如果list集合的元素数量少于顶点数量则说明该有向图存在环。

可以看到它的思想还是比较简单的,对一个具有n个顶点e条弧的AOV网来说, Kahn算法的时间复杂度为O(n+e)。

2.2 案例分析

该案例对应着下面实现代码中的案例,这里以辅助结构为队列来分析。

首先,构建一个有向无环图,案例中的顶点构成的有向无环图如下:

首先将每个顶点的入度都加入到该顶点对应索引的辅助数组中,然后将入度为0的顶点都加入到队列中,此时顶点入度数组为{0,2,1,2,2,1,1},队列为{”A”}。

下面开始循环判断队列是否为空。

第一次判断时肯定不为空,因为有一个顶点“A”,在循环体中取出队列头部元素,此时是取出了A这个顶点,然后加入到辅助list集合中,该集合中顶点的顺序就是拓扑排序的顶点元素的顺序。

然后获取该顶点的邻接点,由于“A”顶点的入度为0,且被“移除”了,因此“A”的邻接点的边也要“移除”,因此所有哦邻接点的入度都要减去1,在每一个邻接点的入度减去一之后,判断该邻接点的入度值是否等于0,如果是等于0,那么说明该顶点作为遍历的起点,此时需要被加入到辅助队列中。再第一次大循环之后,顶点入度数组为{0,2,0,1,2,0,1},辅助队列为{”C”,”F”},结果集result为{”A”}。

此时排除被“删除”的顶点和边,图的结构如下:

可以看到顶点变成了“C”、“F”。

由于辅助队列还有元素,因此开始第二次循环。“移除”队头元素,此时取出“C”,将“C”加入result结果集,获取“C”的邻接点,删除C与邻接点相连的边。因此邻接点的入度需要减去1,明显此时“D”点的入度变成了0,此时将“D”加入队尾。再第二次大循环之后,顶点入度数组为{0,1,0,0,2,0,1},辅助队列为{”F”,”B”},结果集result为{”A”,”C”}。

此时排除被“删除”的顶点的边,图的结构如下:

可以看到顶点变成了“F”、“D”。

由于辅助队列还有元素,因此开始第三次循环。“移除”队头元素,此时取出“F”,将“F”加入result结果集,获取“F”的邻接点,删除“F”与邻接点相连的边。因此邻接点的入度需要减去1,明显此时“G”点的入度变成了0,此时将“G”加入队尾。在第三次大循环之后,顶点入度数组为{0,1,0,0,2,0,0},辅助队列为{”B” ,”G”},结果集result为{”A”,”C”,”F”}。

此时排除被“删除”的顶点的边,图的结构如下:

可以看到顶点变成了“D”、“G”。

由于辅助队列还有元素,因此开始第四次循环。“移除”队头元素,此时取出“D”,将“D”加入result结果集,获取“D”的邻接点,删除“D”与邻接点相连的边。因此邻接点的入度需要减去1,明显此时“B”点的入度变成了0,此时将“B”加入队尾。在第四次大循环之后,顶点入度数组为{0,0,0,0,2,0,0},辅助队列为{”G” ,”B”},结果集result为{”A”,”C”,”F”,”D”}。

此时排除被“删除”的顶点的边,图的结构如下:

可以看到顶点变成了“G”、“B”。

由于辅助队列还有元素,因此开始第五次循环。“移除”队头元素,此时取出“G”,将“G”加入result结果集,获取“G”的邻接点,删除“G”与邻接点相连的边。因此邻接点的入度需要减去1,但是此时“E”点的入度并没有变成了0,而是1,因为还有一个B点通向“E”点。在第五次大循环之后,顶点入度数组为{0,0,0,0,1,0,0},辅助队列为{”B”},结果集result为{”A”,”C”,”F”,”D”,”G”}。

此时排除被“删除”的顶点的边,图的结构如下:

可以看到顶点变成了“B”。

由于辅助队列还有元素,因此开始第六次循环。“移除”队头元素,此时取出“B”,将“B”加入result结果集,获取“B”的邻接点,删除“B”与邻接点相连的边。因此邻接点的入度需要减去1,明显此时“E”点的入度变成了0,此时将“E”加入队尾。在第六次大循环之后,顶点入度数组为{0,0,0,0,0,0,0},辅助队列为{”E”},结果集result为{”A”,”C”,”F”,”D”,”G”,”B”}。

此时排除被“删除”的顶点的边,图的结构如下:

可以看到顶点变成了“E”。

由于辅助队列还有元素,因此开始第七次循环。“移除”队头元素,此时取出“E”,将“E”加入result结果集,获取“E”的邻接点,删除“E”与邻接点相连的边。因此邻接点的入度需要减去1,但是此时“E”点并没有邻接点,因此第七次大循环结束。在第七次大循环之后,顶点入度数组为{0,0,0,0,0,0,0},辅助队列为{},结果集result为{”A”,”C”,”F”,”D”,”G”,”B”,”E”}。

此时排除被“删除”的顶点的边,图的结构如下:

可以看到没有了顶点,即辅助队列为空,此时大循环结束,程序结束,输出result,顺序为{”A”,”C”,”F”,”D”,”G”,”B”,”E”}。实际上这就是拓扑排序的一种合理的顺序。

当我们使用的辅助结构是栈空间时,获得的顺序可能是{”A”,”F”,”G”,”C”,”D”,”B”,”E”} 。

实际上,图中能够明确确定的顺序,即偏序顺序为:

A先于C、D、F;
C先于D、B;
D先于B;
B先于E;
F先于G;
G先于E;

我们再看上面获得的两个顺序序列,实际上这两种顺序都是合理的,完全满足上面的偏序条件,并且给出了两种全序顺序,并且我们可以知道还有更多的复合规则的全序顺序没有求出来,这也正是拓扑排序顺序的不唯一性的表现。

3 邻接矩阵有向图实现

这里的实现能够构造一个基于邻接矩阵实现有向图的类;并且提供深度优先遍历和广度优先遍历的方法,提供基于Kahn算法的获取拓扑序列的方法。

/*** 邻接矩阵有向图实现Kahn算法* {@link MatrixKahn#MatrixKahn(E[], E[][])}  构建有向图* {@link MatrixKahn#DFS()}  深度优先遍历无向图* {@link MatrixKahn#BFS()}  广度优先遍历无向图* {@link MatrixKahn#toString()} 输出无向图* {@link MatrixKahn#kahn()} Kahn算法获取拓扑序列** @param <E>* @author lx*/
public class MatrixKahn<E> {/*** 顶点数组*/private Object[] vertexs;/*** 邻接矩阵*/private int[][] matrix;/*** 创建有向图** @param vexs  顶点数组* @param edges 边二维数组*/public MatrixKahn(E[] vexs, E[][] edges) {// 初始化顶点数组,并添加顶点vertexs = Arrays.copyOf(vexs, vexs.length);// 初始化边矩阵,并填充边信息matrix = new int[vexs.length][vexs.length];for (E[] edge : edges) {// 读取一条边的起始顶点和结束顶点索引值 p1,p2表示边方向p1->p2int p1 = getPosition(edge[0]);int p2 = getPosition(edge[1]);//p1 出度的位置 置为1matrix[p1][p2] = 1;//无向图和有向图的邻接矩阵实现的区别就在于下面这一行代码//matrix[p2][p1] = 1;}}/*** 获取某条边的某个顶点所在顶点数组的索引位置** @param e 顶点的值* @return 所在顶点数组的索引位置, 或者-1 - 表示不存在*/private int getPosition(E e) {for (int i = 0; i < vertexs.length; i++) {if (vertexs[i] == e) {return i;}}return -1;}/*** 深度优先搜索遍历图,类似于树的前序遍历,*/public void DFS() {//新建顶点访问标记数组,对应每个索引对应相同索引的顶点数组中的顶点boolean[] visited = new boolean[vertexs.length];//初始化所有顶点都没有被访问for (int i = 0; i < vertexs.length; i++) {visited[i] = false;}System.out.println("DFS:");System.out.print("\t");for (int i = 0; i < vertexs.length; i++) {if (!visited[i]) {DFS(i, visited);}}System.out.println();}/*** 深度优先搜索遍历图的递归实现,类似于树的先序遍历* 因此模仿树的先序遍历,同样借用栈结构,这里使用的是方法的递归,隐式的借用栈** @param i       顶点索引* @param visited 访问标志数组*/private void DFS(int i, boolean[] visited) {visited[i] = true;System.out.print(vertexs[i] + " ");// 遍历该顶点的所有邻接点。若该邻接点是没有访问过,那么继续递归遍历领接点for (int w = firstVertex(i); w >= 0; w = nextVertex(i, w)) {if (!visited[w]) {DFS(w, visited);}}}/*** 返回顶点v的第一个邻接顶点的索引,失败则返回-1** @param v 顶点v在数组中的索引* @return 返回顶点v的第一个邻接顶点的索引,失败则返回-1*/private int firstVertex(int v) {//如果索引超出范围,则返回-1if (v < 0 || v > (vertexs.length - 1)) {return -1;}/*根据邻接矩阵的规律:顶点索引v对应着边二维矩阵的matrix[v][i]一行记录* 从i=0开始*/for (int i = 0; i < vertexs.length; i++) {if (matrix[v][i] == 1) {return i;}}return -1;}/*** 返回顶点v相对于w的下一个邻接顶点的索引,失败则返回-1** @param v 顶点索引* @param w 第一个邻接点索引* @return 返回顶点v相对于w的下一个邻接顶点的索引,失败则返回-1*/private int nextVertex(int v, int w) {//如果索引超出范围,则返回-1if (v < 0 || v > (vertexs.length - 1) || w < 0 || w > (vertexs.length - 1)) {return -1;}/*根据邻接矩阵的规律:顶点索引v对应着边二维矩阵的matrix[v][i]一行记录* 由于邻接点w的索引已经获取了,所以从i=w+1开始寻找*/for (int i = w + 1; i < vertexs.length; i++) {if (matrix[v][i] == 1) {return i;}}return -1;}/*** 广度优先搜索图,类似于树的层序遍历* 因此模仿树的层序遍历,同样借用队列结构*/public void BFS() {// 辅组队列Queue<Integer> indexLinkedList = new LinkedList<>();//新建顶点访问标记数组,对应每个索引对应相同索引的顶点数组中的顶点boolean[] visited = new boolean[vertexs.length];for (int i = 0; i < vertexs.length; i++) {visited[i] = false;}System.out.println("BFS:");System.out.print("\t");for (int i = 0; i < vertexs.length; i++) {if (!visited[i]) {visited[i] = true;System.out.print(vertexs[i] + " ");indexLinkedList.add(i);}if (!indexLinkedList.isEmpty()) {//j索引出队列Integer j = indexLinkedList.poll();//继续访问j的邻接点for (int k = firstVertex(j); k >= 0; k = nextVertex(j, k)) {if (!visited[k]) {visited[k] = true;System.out.print(vertexs[k] + " ");//继续入队列indexLinkedList.add(k);}}}}System.out.println();}@Overridepublic String toString() {StringBuilder stringBuilder = new StringBuilder();for (int i = 0; i < vertexs.length; i++) {for (int j = 0; j < vertexs.length; j++) {stringBuilder.append(matrix[i][j]).append("\t");}stringBuilder.append("\n");}return stringBuilder.toString();}/*** kahn算法求拓扑排序*/public void kahn() {//用于存储顶点的入度的数组int[] inArr = new int[vertexs.length];//遍历矩阵,计算每个顶点的入度for (int i = 0; i < vertexs.length; i++) {for (int j = 0; j < vertexs.length; j++) {if (matrix[i][j] != 0) {inArr[j]++;}}}//辅助结构队列,用于存储0入度的顶点Queue<Integer> queueNode = new LinkedList<>();//辅助栈空间,用于存储0入度的顶点//Stack<Integer> stackNode = new Stack<>();for (int i = 0; i < inArr.length; i++) {if (inArr[i] == 0) {//添加0入度的顶点索引到队列尾部queueNode.add(i);//添加0入度的顶点索引到栈顶//stackNode.add(i);}}List<Integer> result = new ArrayList<>();//入度为0的节点从队列弹出并且把加入list,相当于从图中去掉,所以还要把其邻接节点的入度减1//循环判断队列是否为空while (!queueNode.isEmpty()) {//入度为0的节点索引从队列头部移除并且加入resultInteger nodeIndex = queueNode.poll();//实际上存储顺序就是拓扑排序的顺序result.add(nodeIndex);//遍历矩阵,获取该顶点的邻接点,将邻接点的入度减去一,并且判断邻接点的入度是否变成了0,如果变成了0那么也加入到队列中for (int i = 0; i < vertexs.length; i++) {//入度在顶点所表示的"列"中if (matrix[nodeIndex][i] != 0) {if (--inArr[i] == 0) {queueNode.add(i);}}}}/*使用栈辅助结构*///循环判断栈是否为空/*while (!stackNode.isEmpty()) {//移除栈顶顶点元素索引Integer nodeIndex = stackNode.pop();//实际上存储顺序就是拓扑排序的顺序result.add(nodeIndex);//获取该顶点的邻接点,将邻接点的入度减去一,并且判断邻接点的入度是否变成了0,如果变成了0那么也加入到队列中for (int i = 0; i < vertexs.length; i++) {if (matrix[nodeIndex][i] != 0) {if (--inArr[i] == 0) {stackNode.add(i);}}}}*///输出集合,顺出顺序就是拓扑排序的顺序System.out.println("Kahn:");System.out.print("\t");for (Integer nodeIndex : result) {System.out.print(vertexs[nodeIndex] + " ");}}public static void main(String[] args) {Character[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};Character[][] edges = {{'A', 'C'},{'A', 'D'},{'A', 'F'},{'C', 'B'},{'C', 'D'},//{'D', 'C'},{'D', 'B'},{'G', 'E'},{'B', 'E'},{'F', 'G'}};//构建图MatrixKahn<Character> matrixKahn = new MatrixKahn<>(vexs, edges);//输出图System.out.println(matrixKahn);//深度优先遍历matrixKahn.DFS();//广度优先遍历matrixKahn.BFS();//Kahn算法求拓扑序列matrixKahn.kahn();}
}

4 邻接表有向图实现

这里的实现能够构造一个基于邻接表实现有向图的类;并且提供深度优先遍历和广度优先遍历的方法,提供基于Kahn算法的获取拓扑序列的方法。

/*** 邻接表有向图实现Kahn算法* {@link ListKahn#ListKahn(E[], E[][])}  构建有向图* {@link ListKahn#DFS()}  深度优先遍历有向图* {@link ListKahn#BFS()}  广度优先遍历有向图* {@link ListKahn#toString()} 输出有向图* {@link ListKahn#kahn()} Kahn算法获取拓扑序列** @param <E>* @author lx*/
public class ListKahn<E> {/*** 顶点类** @param <E>*/private class Node<E> {/*** 该顶点的入度*/int in;/*** 顶点信息*/E data;/*** 指向第一条依附该顶点的边*/LNode firstEdge;public Node(E data, LNode firstEdge) {this.data = data;this.firstEdge = firstEdge;}@Overridepublic String toString() {return "" + data;}}/*** 边表节点类*/private class LNode {/*** 该边所指向的顶点的索引位置*/int vertex;/*** 指向下一条弧的指针*/LNode nextEdge;}/*** 顶点数组*/private Node<E>[] vertexs;/*** 创建图** @param vexs  顶点数组* @param edges 边二维数组*/public ListKahn(E[] vexs, E[][] edges) {/*初始化顶点数组,并添加顶点*/vertexs = new Node[vexs.length];for (int i = 0; i < vertexs.length; i++) {vertexs[i] = new Node<>(vexs[i], null);}/*初始化边表,并添加边节点到边表尾部,即采用尾插法*/for (E[] edge : edges) {// 读取一条边的起始顶点和结束顶点索引值int p1 = getPosition(edge[0]);int p2 = getPosition(edge[1]);// 初始化lnode1边节点 即表示p1指向p2的边LNode lnode1 = new LNode();lnode1.vertex = p2;// 将LNode链接到"p1所在链表的末尾"if (vertexs[p1].firstEdge == null) {vertexs[p1].firstEdge = lnode1;} else {linkLast(vertexs[p1].firstEdge, lnode1);}for (Node<E> vertex : vertexs) {if (vertex.data.equals(edge[1])) {vertex.in += 1;}}}}/*** 获取某条边的某个顶点所在顶点数组的索引位置** @param e 顶点的值* @return 所在顶点数组的索引位置, 或者-1 - 表示不存在*/private int getPosition(E e) {for (int i = 0; i < vertexs.length; i++) {if (vertexs[i].data == e) {return i;}}return -1;}/*** 将lnode节点链接到边表的最后,采用尾插法** @param first 边表头结点* @param node  将要添加的节点*/private void linkLast(LNode first, LNode node) {while (true) {if (first.vertex == node.vertex) {return;}if (first.nextEdge == null) {break;}first = first.nextEdge;}first.nextEdge = node;}/*** 深度优先搜索遍历图的递归实现,类似于树的先序遍历* 因此模仿树的先序遍历,同样借用栈结构,这里使用的是方法的递归,隐式的借用栈** @param i       顶点索引* @param visited 访问标志数组*/private void DFS(int i, boolean[] visited) {//索引索引标记为true ,表示已经访问了visited[i] = true;System.out.print(vertexs[i].data + " ");//获取该顶点的边表头结点LNode node = vertexs[i].firstEdge;//循环遍历该顶点的邻接点,采用同样的方式递归搜索while (node != null) {if (!visited[node.vertex]) {DFS(node.vertex, visited);}node = node.nextEdge;}}/*** 深度优先搜索遍历图,类似于树的前序遍历,*/public void DFS() {//新建顶点访问标记数组,对应每个索引对应相同索引的顶点数组中的顶点boolean[] visited = new boolean[vertexs.length];//初始化所有顶点都没有被访问for (int i = 0; i < vertexs.length; i++) {visited[i] = false;}System.out.println("DFS:");System.out.print("\t");/*循环搜索*/for (int i = 0; i < vertexs.length; i++) {//如果对应索引的顶点的访问标记为false,则搜索该顶点if (!visited[i]) {DFS(i, visited);}}/*走到这一步,说明顶点访问标记数组全部为true,说明全部都访问到了,深度搜索结束*/System.out.println();}/*** 广度优先搜索图,类似于树的层序遍历* 因此模仿树的层序遍历,同样借用队列结构*/public void BFS() {// 辅组队列Queue<Integer> indexLinkedList = new LinkedList<>();//新建顶点访问标记数组,对应每个索引对应相同索引的顶点数组中的顶点boolean[] visited = new boolean[vertexs.length];//初始化所有顶点都没有被访问for (int i = 0; i < vertexs.length; i++) {visited[i] = false;}System.out.println("BFS:");System.out.print("\t");for (int i = 0; i < vertexs.length; i++) {//如果访问方剂为false,则设置为true,表示已经访问,然后开始访问if (!visited[i]) {visited[i] = true;System.out.print(vertexs[i].data + " ");indexLinkedList.add(i);}//判断队列是否有值,有就开始遍历if (!indexLinkedList.isEmpty()) {//出队列Integer j = indexLinkedList.poll();LNode node = vertexs[j].firstEdge;while (node != null) {int k = node.vertex;if (!visited[k]) {visited[k] = true;System.out.print(vertexs[k].data + " ");//继续入队列indexLinkedList.add(k);}node = node.nextEdge;}}}System.out.println();}@Overridepublic String toString() {StringBuilder stringBuilder = new StringBuilder();for (int i = 0; i < vertexs.length; i++) {stringBuilder.append(i).append("(").append(vertexs[i].data).append("-").append(vertexs[i].in).append("): ");LNode node = vertexs[i].firstEdge;while (node != null) {stringBuilder.append(node.vertex).append("(").append(vertexs[node.vertex].data).append(")");node = node.nextEdge;if (node != null) {stringBuilder.append("->");} else {break;}}stringBuilder.append("\n");}return stringBuilder.toString();}/*** kahn算法求拓扑排序*/public void kahn() {//用于存储顶点的入度的数组int[] inArr = new int[vertexs.length];//辅助结构队列,用于存储0入度的顶点Queue<Node<E>> queueNode = new LinkedList<>();//辅助栈空间,用于存储0入度的顶点Stack<Node<E>> stackNode = new Stack<>();for (int i = 0; i < vertexs.length; i++) {inArr[i] = vertexs[i].in;if (vertexs[i].in == 0) {//添加0入度的顶点到队列尾部queueNode.add(vertexs[i]);//添加0入度的顶点到栈顶stackNode.add(vertexs[i]);}}List<Node<E>> result = new ArrayList<>();// 入度为0的节点从队列弹出并且把加入list,相当于从图中去掉,所以还要把其邻接节点的入度减1//循环判断队列是否为空while (!queueNode.isEmpty()) {//入度为0的节点从队列头部移除并且加入resultNode<E> node = queueNode.poll();//实际上存储顺序就是拓扑排序的顺序result.add(node);//获取该顶点的邻接点,将邻接点的入度减去一,并且判断邻接点的入度是否变成了0,如果变成了0那么也加入到队列中LNode first = node.firstEdge;while (first != null) {inArr[first.vertex]--;if (inArr[first.vertex] == 0) {queueNode.add(vertexs[first.vertex]);}first = first.nextEdge;}}/*使用栈辅助结构*///循环判断栈是否为空/*while (!stackNode.isEmpty()) {//移除栈顶顶点元素Node<E> node = stackNode.pop();//实际上存储顺序就是拓扑排序的顺序result.add(node);//获取该顶点的领接点,将领接点的入度减去一,并且判断领接点的入度是否变成了0,如果变成了0那么也加入到队列中LNode first = node.firstEdge;while (first != null) {inArr[first.vertex]--;if (inArr[first.vertex] == 0) {stackNode.add(vertexs[first.vertex]);}first = first.nextEdge;}}*///输出集合,顺出顺序就是拓扑排序的顺序System.out.println("Kahn:");System.out.print("\t");System.out.println(result);}public static void main(String[] args) {//顶点数组 添加的先后顺序对于遍历结果有影响Character[] vexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};//边二维数组 {'a', 'b'}表示顶点a->b的边  添加的先后顺序对于遍历结果有影响Character[][] edges = {{'A', 'C'},{'A', 'D'},{'A', 'F'},{'C', 'B'},{'C', 'D'},//{'D', 'C'},{'D', 'B'},{'G', 'E'},{'B', 'E'},{'F', 'G'}};// 构建图有向图ListKahn<Character> listKahn = new ListKahn<>(vexs, edges);//输出图System.out.println(listKahn);//深度优先遍历listKahn.DFS();//广度优先遍历listKahn.BFS();//Kahn算法求拓扑序列listKahn.kahn();}
}

相关参考:

  1. 《算法》
  2. 《数据结构与算法》
  3. 《大话数据结构》
  4. 《算法图解》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

图论算法—图的拓扑排序介绍和Kahn算法原理解析以及Java代码的实现相关推荐

  1. 拓扑排序:利用kahn算法。c++

    拓扑排序 拓扑排序要解决的问题是给一个图的所有节点排序.在一个有向无环图DAG中,我们将图中的顶点以线性的方式进行排序,对于有向边(u,v),确保u在v的前面,认为v依赖于u; 如果u到v有路径,u可 ...

  2. 数据结构与算法之-----图(拓扑排序)

    [​​​​​​​ 写在前面的话:本专栏的主要内容:数据结构与算法. 1.对于​​​​​​​初识数据结构的小伙伴们,鉴于后面的数据结构的构建会使用到专栏前面的内容,包括具体数据结构的应用,所使用到的数据 ...

  3. 【图论】有向无环图的拓扑排序

    1. 引言 有向无环图(Directed Acyclic Graph, DAG)是有向图的一种,字面意思的理解就是图中没有环.常常被用来表示事件之间的驱动依赖关系,管理任务之间的调度.拓扑排序是对DA ...

  4. 有向无环图DAG 拓扑排序 代码解释

    目录: DAG定义 举例描述 实际运用 算法描述 算法实战 算法可视化 定义 在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序(英语:Topological ...

  5. 第5-2课:图的拓扑排序

    拓扑排序常用来确定一个依赖关系集中.事物发生的顺序.一个典型的应用场景就是在项目管理或工程实施中安排各种生产活动的计划,在考虑各种活动的依赖关系的基础上,安排各种活动的开始时间,使得项目或工程能够以高 ...

  6. 数据结构-考研难点代码突破 (C++实现有向无环图的拓扑排序)

    文章目录 1. AOV网 2. 拓扑排序 C++代码 1. AOV网 AOV网∶若用DAG 图(有向无环图)表示一个工程,其顶点表示活动,用有向边<Vi,Vj>表示活动 Vi必须先于活动V ...

  7. python 拓扑排序_拓扑排序(topsort)算法详解

    在图论中,由某个集合上的偏序得到全序的策略就是拓补排序算法.拓扑排序常出现在涉及偏序关系的问题中,例如时序的先后.事物的依赖等.针对这些问题拓扑排序通常能有效地给出可行解. 为了便于理解,我们先来看一 ...

  8. 拓扑排序 php,数据结构与算法(周测7-拓扑排序和AOV网络)

    判断题 1.AOE图的关键路径就是最长的路径 T F 2.AOE图的权值最大的边(活动)一定是关键活动. T F 两条边相加可能比最大的边还要大. 3.在AOE-网工程中,减少任一关键活动上的权值后, ...

  9. 有向无环图的拓扑排序

    拓扑排序 对于一个有向无环图,我们可以这样确定一个图中顶点的顺序:  对于所有的u.v,若存在有向路径u-->v,则在最后的顶点排序中u就位于v之前.这样确定的顺序就是一个图的拓扑排序.     ...

最新文章

  1. 极大似然估计(Maximum Likelihood Estimattion Theory)是什么?极大似然估计的本质思想是什么?为什么极大似然可以作为损失函数使用?负对数似然损失函数(Negative
  2. C#设计模式(8)-Builder Pattern
  3. C# DirectX 开发2 - 定义一个矩阵和赋值
  4. linux手机投屏软件,无线投屏器如此多,到底哪一种才合适企业用?
  5. java arrays方法_Java工具类Arrays中不得不知的常用方法
  6. jvm垃圾回收机制_JVM 垃圾回收机制之堆的分代回收
  7. python更新数据库表的时间字段_python更新数据库中某个字段的数据(方法详解)
  8. 联合使用 HTML 5、地理定位 API
  9. matlab机器人自动分拣_极智嘉分拣系统落地 助力打造智慧物流引擎
  10. python 内置_python 内置
  11. 使用socks5代理实现SSH安全登录
  12. 罪恶都市联机器无法显示服务器,《GTA:罪恶都市》多人联机!你的童年又回来了,梦想成真了!...
  13. MATLAB-图像分割
  14. windows屏幕放大镜
  15. 3.4 智能手表整体结构设计总结
  16. 解决 git reject
  17. 51单片机八段数码c语言程序,51单片机做的音乐盒,带八段数码管显示程序+Proteus仿真...
  18. HashMap方法tableSizeFor解析
  19. cmwap和cmnet的区别
  20. 中国科学技术大学计算机科学夏令营,中国科学技术大学计算机科学2019年推免夏令营通知...

热门文章

  1. 机器学习中关于偏差、方差和误差的理解
  2. 线性代数(Linear Algebra)
  3. 蓝桥杯15单片机软件环境搭建
  4. html中单选怎么写,创建一个单选框的html代码是
  5. xshell 6 打开出现错误提示FlexNet_Licensing
  6. Linux 时间同步systemd-timesyncd介绍
  7. 联通TEWA-800E超级用户改桥接模式
  8. DGIOT国内首家轻量级物联网开源平台——支持远程打印条码/二维码和一码设备全生命周期管理
  9. usb由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个硬件设备(代码 19)
  10. css文本样式有,CSS文本常用样式