「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」。
详细介绍了图的拓扑排序的概念,然后介绍了求拓扑序列的算法:Kahn算法的原理,最后提供了基于邻接矩阵和邻接表的图对该算法的Java实现。
阅读本文需要一定的图的基础,如果对于图不是太明白的可以看看这篇文章:图的入门概念以及存储结构、遍历方式介绍和Java代码的实现。
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算法的基本思想是:
- 找到入度为0 的顶点找到并记录到队列或者栈中;
- 移除找到的入度为0的顶点和对应的以该顶点为起点的边,并将被移除的顶点加入到list集合中,同时移除的顶点作为起点的边的终点的如度减去1;继续循环1的步骤,直至队列或者栈为空。
- 此时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->p2
int p1 = getPosition(edge[0]);
int p2 = getPosition(edge[1]);
//p1 出度的位置 置为1
matrix[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) {
//如果索引超出范围,则返回-1
if (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) {
//如果索引超出范围,则返回-1
if (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();
}
@Override
public 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的节点索引从队列头部移除并且加入result
Integer 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;
}
@Override
public 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();
}
@Override
public 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的节点从队列头部移除并且加入result
Node<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();
}
}
相关参考:
- 《算法》
- 《数据结构与算法》
- 《大话数据结构》
- 《算法图解》
如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!