基本概念:
一个无环的有向图称为 有向无环图(Directed Acycline Graph,DAG)。
有向无环图是描述一个工程、计划、生产、系统等流程的有效工具。一个大工程可分为 若干个子工程(活动),活动之间通常有一定的约束,例如先做什么活动、后做什么活动。
用顶点表示活动,用弧表示活动之间的优先关系的有向图,称为顶点表示活动的网 (Activity On Vertex Network),简称 AOV 网。
拓扑排序 是指将 AOV 网中的顶点排成一个线性序列,该序列必须满足:若从顶点 i 到顶点 j 有一条路径,则该序列中 顶点 i 一定在顶点 j 之前。拓扑排序的基本思想如下:
1)选择一个无前驱的顶点并输出。
2)从图中删除该顶点和该顶点的所有发出边。
3)重复第 1 步和第 2 步,直到不存在无前驱的顶点。
4)如果输出的顶点数小于 AOV 网中的顶点数,则说明网中有环,否则输出的序列即拓扑序列。
需要注意的是,拓扑排序并不是唯一的,当存在多个无前驱的顶点时,可以选择其中任意一个输出。
上述的描述过程中有删除顶点和边的操作,实际上,完全没必要真的删除顶点和边。可 以将没有前驱的顶点(入度为 0)暂存到栈中,输出时出栈即表示删除。边的删除只需要将 其邻接点的入度减 1 即可。
拓扑排序的算法步骤如下:
1)求出各顶点的入度,存入数组 indegree[]中,并将入度为 0 的顶点入栈 S。
2)如果栈不空,则重复执行以下操作:
• 栈顶元素 i 出栈,并保存到拓扑序列数组 topo[]中;
• 顶点 i 的所有邻接点入度减 1,如果减 1 后入度为 0,立即入栈 S。
3)如果输出的顶点数小于 AOV 网中的顶点数,则说明网中有环,否则输出拓扑序列。
算法复杂度分析
(1)时间复杂度 求有向图中各顶点的入度需要遍历邻接表,算法的时间复杂度为 O(e)。度数为 0 的顶点 入栈的时间复杂度为 O(n),若有向图无环,每个顶点出栈后其邻接点入度减 1,时间复杂度为 O(e)。总的时间复杂度为 O(n+e)。
(2)空间复杂度 算法所需要的辅助空间包含入度数组 indegree[]、拓扑序列数组 topo[]、栈 S,则算法的 空间复杂度是 O(n)。
参考:《趣学数据结构》作者:陈小玉
图有很多种存储方法,其中,邻接矩阵和邻接表是两种简单且常用的方法。
在进行拓扑排序时,因为删除顶点 i 时,要将顶点 i 的所有邻接点入度减 1,访问一个顶点的所有邻接点, 使用邻接表存储需要时间复杂度为该顶点的度,邻接矩阵需要 O(n),因此这里采用邻接表存储。
有向图邻接表求出度容易,而逆邻接表求入度容易。
package com.ymzh.demo4IoC;
import java.util.*;
public class Util4DAG {
private static class Reference {
private final Integer bean;
private final Integer ref;
public Reference (Integer bean, Integer ref) {
this.bean = bean;
this.ref = ref;
}
public Integer getBean() {
return bean;
}
public Integer getRef() {
return ref;
}
}
/**
* 根据边集合生成图的邻接表,并计算入度
* @param references 边集合
* @param graph 图的邻接表
* @param inDegree 图的入度
*/
public static void CreateGraphAndInDegree(List<Reference> references, Map<Integer, List<Integer>> graph, Map<Integer, Integer> inDegree){
// 初始化 邻接表 和 入度
for (Reference reference : references) {
if (!graph.containsKey(reference.getBean())) {
graph.put(reference.getBean(), new ArrayList<>());
inDegree.put(reference.getBean(), 0);
}
}
// 创建邻接表, 并计算入度
for (Reference reference : references) {
if(reference.getRef() != null) {
graph.get(reference.getRef()).add(reference.getBean());
inDegree.put(reference.getBean(), inDegree.get(reference.getBean())+1);
}
}
}
/**
*
* @param graph 图的邻接表
* @param inDegree 图的入度
* @param topo 图的拓扑排序
* @return 是否存在拓扑排序
* 若 G 无回路,则生成 G 的一个拓扑序列 topo[],并返回 true,否则 false
*/
public static boolean TopologicalSort(Map<Integer, List<Integer>> graph, Map<Integer, Integer> inDegree, List topo) {
// 辅助栈
Deque<Integer> stack = new ArrayDeque<>();
//入度为 0 者进栈
for(Integer key : graph.keySet()) {
if(inDegree.get(key) == 0){
stack.addLast(key);
}
}
while (!stack.isEmpty()) { //栈非空
Integer cur = stack.removeLast(); //取栈顶顶点 cur
topo.add(cur); //将 cur 保存在拓扑序列数组 topo 中
// 遍历 cur 的邻接点
for (Integer next : graph.get(cur)) {
inDegree.put(next, inDegree.get(next) - 1); // cur 的每个邻接点的入度减 1
if (inDegree.get(next) == 0) //若入度减为 0, 则入栈
stack.push(next);
}
// 迭代器遍历
// Iterator<Integer> iter = graph.get(cur).iterator();
// while (iter.hasNext()) {
// Integer next = iter.next();
// inDegree.put(next, inDegree.get(next) - 1); // cur 的每个邻接点的入度减 1
// if (inDegree.get(next) == 0) //若入度减为 0, 则入栈
// stack.push(next);
// }
}
// index < inDegree.size() 时, 图存在回路, 不存在拓扑排序
return topo.size() >= inDegree.size();
}
public static void main(String[] args) {
// 实现创建好 用于保存 入度 和 图 的集合。
Map<Integer, Integer> inDegree = new HashMap<>();
Map<Integer, List<Integer>> graph = new HashMap<>();
// 边集合, 其中 ref 为起点, bean 为终点
ArrayList<Reference> references = new ArrayList<>(){{
add(new Reference(0, null));
add(new Reference(1, 0));
add(new Reference(1, 2));
add(new Reference(2, 0));
add(new Reference(3, 0));
add(new Reference(3, 5));
add(new Reference(4, 2));
add(new Reference(4, 3));
add(new Reference(4, 5));
add(new Reference(5, null));
}};
// 根据边集合, 创建 图, 并计算 入度
CreateGraphAndInDegree(references, graph, inDegree);
// 实现创建好 用于 拓扑排序的 集合
ArrayList topo = new ArrayList();
TopologicalSort(graph, inDegree, topo);
System.out.println(topo.toString());
}
}