图--03---有向图拓扑排序
Posted 高高for 循环
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图--03---有向图拓扑排序相关的知识,希望对你有一定的参考价值。
有向图
网络链接
- 在实际生活中,很多应用相关的图都是有方向性的,最直观的就是网络,可以从A页面通过链接跳转到B页面,那么a和b连接的方向是a->b,但不能说是b->a,此时我们就需要使用有向图来解决这一类问题,它和我们之前学习的无向图,最大的区别就在于连接是具有方向的,在代码的处理上也会有很大的不同。
定义:
有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。
相关术语
-
出度:
由某个顶点指出的边的个数称为该顶点的出度。 -
入度:
指向某个顶点的边的个数称为该顶点的入度。 -
有向路径:
由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。 -
有向环:
一条至少含有一条边,且起点和终点相同的有向路径。
一副有向图中两个顶点v和w可能存在以下四种关系:
- 没有边相连;
- 存在从v到w的边v—>w;
- 存在从w到v的边w—>v;
- 既存在w到v的边,也存在v到w的边,即双向连接;
理解有向图是一件比较简单的,但如果要通过眼睛看出复杂有向图中的路径就不是那么容易了。
有向图API设计
- 在api中设计了一个反向图,其因为有向图的实现中,用adj方法获取出来的是由当前顶点v指向的其他顶点,如果能得到其反向图,就可以很容易得到指向v的其他顶点。
代码实现1.------Digraph:
package graph;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
public class Digraph {
//顶点数目
private final int V;
//边的数目
private int E;
//邻接表
private Queue<Integer>[] adj;
public Digraph(int V){
//初始化顶点数量
this.V = V;
//初始化边的数量
this.E = 0;
//初始化邻接表
this.adj = new Queue[V];
for (int i = 0; i < adj.length; i++) {
adj[i] = new ConcurrentLinkedDeque<>();
}
}
//获取顶点数目
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 Queue<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;
}
}
拓扑排序
- 在现实生活中,我们经常会同一时间接到很多任务去完成,但是这些任务的完成是有先后次序的。以我们学习java学科为例,我们需要学习很多知识,但是这些知识在学习的过程中是需要按照先后次序来完成的。从java基础,到jsp/servlet,到ssm,到springboot等是个循序渐进且有依赖的过程。在学习jsp前要首先掌握java基础和html基础,学习ssm框架前要掌握jsp/servlet之类才行。
- 为了简化问题,我们使用整数为顶点编号的标准模型来表示这个案例:
此时如果某个同学要学习这些课程,就需要指定出一个学习的方案,我们只需要对图中的顶点进行排序,让它转换为一个线性序列,就可以解决问题,这时就需要用到一种叫拓扑排序的算法。
拓扑排序定义:
给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级。
检测有向图中的环
- 如果学习x课程前必须先学习y课程,学习y课程前必须先学习z课程,学习z课程前必须先学习x课程,那么一定是有问题了,我们就没有办法学习了,因为这三个条件没有办法同时满足。其实这三门课程x、y、z的条件组成了一个环:
因此,如果我们要使用拓扑排序解决优先级问题,首先得保证图中没有环的存在。
检测有向环的API设计
检测有向环实现
在API中添加了onStack[] 布尔数组,索引为图的顶点,当我们深度搜索时:
- 在如果当前顶点正在搜索,则把对应的onStack数组中的值改为true,标识进栈;
- 如果当前顶点搜索完毕,则把对应的onStack数组中的值改为false,标识出栈;
- 如果即将要搜索某个顶点,但该顶点已经在栈中,则图中有环;
代码实现2. ----DirectedCycle
package graph;
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;
}
}
顶点排序实现
- 在API的设计中,我们添加了一个栈reversePost用来存储顶点,当我们深度搜索图时,每搜索完毕一个顶点,把该顶点放入到reversePost中,这样就可以实现顶点排序。
代码实现3. ------DepthFirstOrder
package graph;
import java.util.Stack;
public class DepthFirstOrder {
//索引代表顶点,值表示当前顶点是否已经被搜索
private boolean[] marked;
//使用栈,存储顶点序列
private Stack<Integer> reversePost;
//创建一个检测环对象,检测图G中是否有环
public DepthFirstOrder(Digraph G){
//初始化marked数组
this.marked = new boolean[G.V()];
//初始化reversePost栈
this.reversePost = new Stack<Integer>();
//遍历图中的每一个顶点,让每个顶点作为入口,完成一次深度优先搜索
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 Stack<Integer> reversePost(){
return reversePost;
}
}
拓扑排序实现
- 前面已经实现了环的检测以及顶点排序,那么拓扑排序就很简单了,基于一幅图,先检测有没有环,如果没有环,则调用顶点排序即可。
API设计:
代码实现4. ------TopoLogical
import java.util.Stack;
public class TopoLogical {
//顶点的拓扑排序
private Stack<Integer> order;
//构造拓扑排序对象
public TopoLogical(Digraph G) {
//创建一个检测有向环的对象
DirectedCycle cycle = new DirectedCycle(G);
//判断G图中有没有环,如果没有环,则进行顶点排序:创建一个顶点排序对象
if (!cycle.hasCycle()){
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G);
order = depthFirstOrder.reversePost();
}
}
//判断图G是否有环
private boolean isCycle(){
return order==null;
}
//获取拓扑排序的所有顶点
public Stack<Integer> order(){
return order;
}
}
测试:
package graph;
import java.util.Stack;
public class TopoLogicalTest {
public static void main(String[] args) {
//准备有向图
Digraph digraph = new Digraph(6);
digraph.addEdge(0, 2);
digraph.addEdge(0, 3);
digraph.addEdge(2, 4);
digraph.addEdge(3, 4);
digraph.addEdge(4, 5);
digraph.addEdge(1, 3);
//通过TopoLogical对象堆有向图中的顶点进行排序
TopoLogical topoLogical = new TopoLogical(digraph);
//获取顶点的线性序列进行打印
Stack<Integer> order = topoLogical.order();
StringBuilder sb = new StringBuilder();
while (!order.empty()) {
sb.append(order.pop() + "->");
}
String str = sb.toString();
str = str.substring(0, str.length() - 2);
System.out.println(str);
}
}
以上是关于图--03---有向图拓扑排序的主要内容,如果未能解决你的问题,请参考以下文章