1 拓扑排序的概念及推导
1.1 引入
在生活中,很多事情都讲求循序渐进。以学习java为例,需要从java基础,到jsp/servlet,到ssm,再到springboot等,是一个循序渐进且有依赖的过程。
为了简化问题,我们使用整数为顶点编号的标准模型来表示这个案例:
要学习这些课程,就需要指定一个循序渐进的方案,即对图中的顶点进行排序,将其转换为一个线性序列,这时就需要用到一种叫拓扑排序的算法。
1.2 概念
拓扑排序:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素;此时就可以明确地表示出每个顶点的优先级。
1.3 检测有向图中的环
如果学校x课程前必须先学习y课程,学习y课程前必须先学习z课程,学习z课程前必须先学习x课程,如此就没有办法学习了,因为三个条件都不能同时满足,x,y,z组成了一个环:
因此,如果要使用拓扑排序解决优先级问题,首先要保证图中没有环的存在。
检测有向环的API设计
- private boolean[] marked: 索引代表顶点,值表示当前顶点是否已经被搜索;
- private boolean[] onStack:索引代表顶点,使用栈的思想,记录当前顶点有没有已经处于正在搜索的有向路径上;
当深度优先搜索时:
- 如果当前顶点正在搜索,则把对应的onStack数组中的值改为true,标识进栈;
- 如果当前顶点搜索完毕,则把对应的onStack数组中的值改为false,标识出栈;
- 如果即将要搜索某个顶点,但该顶点已经在栈中,则图中有环;
public class Digraph {
//顶点数目
private final int V;
//边的数目
private int E;
//邻接表
private Deque<Integer>[] adj;
public Digraph(int V){
//初始化顶点数量
this.V = V;
//初始化边的数量
this.E = 0;
//初始化邻接表
this.adj = new Deque[V];
for (int i = 0; i < adj.length; i++) {
adj[i] = new LinkedList<>();
}
}
//获取顶点数目
public int V(){
return V;
}
//获取边的数目
public int E(){
return E;
}
//向有向图中添加一条边 v->w
public void addEdge(int v, int w) {
//只需要让顶点w出现在顶点v的邻接表中,因为边是有方向的,最终,顶点v的邻接表中存储的相邻顶点的含义是: v->其他顶点
adj[v].offer(w);
E++;
}
//获取由v指出的边所连接的所有顶点
public Deque<Integer> adj(int v){
return adj[v];
}
//该图的反向图
private Digraph reverse(){
//创建有向图对象
Digraph r = new Digraph(V);
for (int v=0; v<V; v++){
//获取由该顶点v指出的所有边
for (Integer w : adj[v]) {//原图中表示的是由顶点v->w的边
r.addEdge(w,v);//w->v
}
}
return r;
}
}
public class DirectedCycle {
//索引代表顶点,值表示当前顶点是否已经被搜索
private boolean[] marked;
//记录图中是否有环
private boolean hasCycle;
//索引代表顶点,使用栈的思想,记录当前顶点有没有已经处于正在搜索的有向路径上
private boolean[] onStack;
//创建一个检测环对象,检测图G中是否有环
public DirectedCycle(Digraph G){
//初始化marked数组
this.marked = new boolean[G.V()];
//初始化hasCycle
this.hasCycle = false;
//初始化onStack数组
this.onStack = new boolean[G.V()];
//找到图中每一个顶点,让每一个顶点作为入口,调用一次dfs进行搜索
for (int v =0; v<G.V();v++){
//判断如果当前顶点还没有搜索过,则调用dfs进行搜索
if (!marked[v]){
dfs(G,v);
}
}
}
//基于深度优先搜索,检测图G中是否有环
private void dfs(Digraph G, int v){
//把顶点v表示为已搜索
marked[v] = true;
//把当前顶点进栈
onStack[v] = true;
//进行深度搜索
for (Integer w : G.adj(v)) {
//判断如果当前顶点w没有被搜索过,则继续递归调用dfs方法完成深度优先搜索
if (!marked[w]) dfs(G,w);
//判断当前顶点w是否已经在栈中,如果已经在栈中,证明当前顶点之前处于正在搜索的状态,那么现在又要搜索一次,证明检测到环了
if (onStack[w]){
hasCycle = true;
return;
}
}
//把当前顶点出栈
onStack[v] = false;
}
//判断当前有向图G中是否有环
public boolean hasCycle(){
return hasCycle;
}
}
public class Test {
public static void main(String[] args) {
Digraph g = new Digraph(5);
g.addEdge(3,0);
g.addEdge(1,0);
g.addEdge(0,2);
g.addEdge(2,1);
g.addEdge(1,4);
DirectedCycle cycle = new DirectedCycle(g);
System.out.println(cycle.hasCycle());
}
}
true
1.4 基于深度优先的顶点排序
添加一个栈reversePost用来存储顶点,当进行深度优先搜索时,每搜索完毕一个顶点,就把该顶点放入到reversePost中,这样就实现了顶点排序。
public class DepthFirstOrder {
//索引代表顶点,值表示当前顶点是否已经被搜索
private boolean[] marked;
//使用栈,存储顶点序列
private Deque<Integer> reversePost;
//创建一个基于深度优先的顶点排序对象,对图G进行顶点排序
public DepthFirstOrder(Digraph G){
//初始化marked数组
this.marked = new boolean[G.V()];
//初始化reversePost栈
this.reversePost = new LinkedList<>();
//遍历图中的每一个顶点,让每个顶点作为入口,完成一次深度优先搜索
for (int v = 0;v<G.V();v++){
if (!marked[v]){
dfs(G,v);
}
}
}
//基于深度优先搜索,把顶点排序
private void dfs(Digraph G, int v){
//标记当前v已经被搜索
marked[v] = true;
//通过循环深度搜索顶点v
for (Integer w : G.adj(v)) {
//如果当前顶点w没有搜索,则递归调用dfs进行搜索
if (!marked[w]){
dfs(G,w);
}
}
//让顶点v进栈
reversePost.push(v);
}
//获取顶点线性序列
public Deque<Integer> reversePost(){
return reversePost;
}
}
public class Test {
public static void main(String[] args) {
Digraph g = new Digraph(6);
g.addEdge(3,0);
g.addEdge(1,0);
g.addEdge(0,2);
g.addEdge(2,1);
g.addEdge(1,4);
DepthFirstOrder cycle = new DepthFirstOrder(g);
System.out.println(cycle.reversePost());
}
}
[1, 0, 3, 2, 4, 5]
2 拓扑排序实现
- 基于一幅图,先检测有没有环(DirectedCycle);
- 如果没有环,再调用顶点排序(DepthFirstOrder),如此就实现了拓扑排序。
public class TopoLogical {
//顶点的拓扑排序
private Deque<Integer> order;
//构造拓扑排序对象
public TopoLogical(Digraph G) {
//创建一个检测有向环的对象
DirectedCycle cycle = new DirectedCycle(G);
//判断G图中有没有环,如果没有环,则进行顶点排序:创建一个顶点排序对象
if (!cycle.hasCycle()){
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G);
order = depthFirstOrder.reversePost();
}
}
//判断图G是否有环
public boolean isCycle(){
return order==null;
}
//获取拓扑排序的所有顶点
public Deque<Integer> order(){
return order;
}
}
public class Test {
public static void main(String[] args) {
Digraph g = new Digraph(6);
g.addEdge(0,2);
g.addEdge(0,3);
g.addEdge(1,3);
g.addEdge(2,4);
g.addEdge(3,4);
g.addEdge(4,5);
TopoLogical cycle = new TopoLogical(g);
System.out.println(cycle.order());
System.out.println(cycle.isCycle());
}
}
[1, 0, 3, 2, 4, 5]
false