数据结构与算法(十一)图的进阶

有向图

在实际生活中,很多应用相关的图都是有方向性的,最直观的就是网络,可以从A页面通过链接跳转到B页面,那么a和b连接的方向是a -> b,但不能说是b -> a,此时我们就需要使用有向图来解决这一类问题,它和我们之前学习的无向图,最大的区别就在于连接是具有方向的,在代码的处理上也会有很大的不同。

有向图的定义及相关术语

定义:有向图是一幅具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。

出度:由某个顶点指出的边的个数称为该顶点的出度。

入度:指向某个顶点的边的个数称为该顶点的入度。

有向路径:由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。

有向环:一条至少含有一条边,且起点和终点相同的有向路径。

一幅有向图中两个顶点v和w可能存在以下四种关系:

  1. 没有边相连;
  2. 存在从v到w的边v -> w
  3. 存在从w到v的边w -> v
  4. 既存在w到v的边,也存在v到w的边,即双向连接;

理解有向图是一件比较简单的,但如果要通过眼睛看出复杂有向图中的路径就不是那么容易了。

有向图API设计

类名 Digraph
构造方法 Digraph(int v):创建一个包含V个顶点但不包含边的有向图
成员方法 public int v():获取图中顶点的数量
public int e():获取图中边的数量
public void addEdge(int v, int w):向有向图中添加一条边v->w
public Queue<Integer> adj(int v):获取由v指出的边所连接的所有顶点
private Digraph reverse():该图的反向图
成员变量 private final int v: 记录顶点数量
private int e:记录边数量
private Queue[] adj:邻接表

在API中设计了一个反向图,其因为有向图的实现中,用adj方法获取出来的是由当前顶点v指向的其他顶点,如果能得到其反向图,就可以很容易得到指向v的其他顶点。

代码实现

public class Digraph {

    /**
     * 记录顶点数量
     */
    private final int v;

    /**
     * 记录边数量
     */
    private int e;

    /**
     * 邻接表
     */
    private Queue<Integer>[] adj;

    /**
     * 创建一个包含V个顶点但不包含边的有向图
     */
    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 Queue<>();
        }
    }

    /**
     * 获取图中顶点的数量
     */
    public int v() {
        return v;
    }

    /**
     * 获取图中边的数量
     */
    public int e() {
        return e;
    }

    /**
     * 向有向图中添加一条边 v->w
     */
    public void addEdge(int v, int w) {
        // 由于有向图中边是有向的,v->w 边,只需要让w出现在v的邻接表中,而不需要让v出现在w的邻接表中
        adj[v].push(w);
        e++;
    }

    /**
     * 获取由v指出的边所连接的所有顶点
     */
    public Queue<Integer> adj(int v) {
        return adj[v];
    }

    /**
     * 该图的反向图
     */
    private Digraph reverse() {
        // 创建新的有向图对象
        var reverse = new Digraph(v);
        // 遍历0~V-1所有顶点,拿到每一个顶点v
        for (int i = 0; i < v; i++) {
            // 得到原图中的v顶点对应的邻接表,原图中的边为 v->w,则反向图中边为w->v;
            for (var w : adj[v]) {
                // 重新添加边,方向为原图的反方向
                reverse.addEdge(w, v);
            }
        }
        return reverse;
    }
}
复制代码

拓扑排序

在现实生活中,我们经常会同一时间接到很多任务去完成,但是这些任务的完成是有先后次序的。以我们学习Java学科为例,我们需要学习很多知识,但是这些知识在学习的过程中是需要按照先后次序来完成的。从Java基础,到JSP/Servlet,到SSM,到SpringBoot等是个循序渐进且有依赖的过程。在学习JSP前要首先掌握Java基础和HTML基础,学习SSM框架前要掌握JSP/Servlet之类才行。

为了简化问题,我们使用整数为顶点编号的标准模型来表示这个案例:

此时如果某个同学要学习这些课程,就需要指定出一个学习的方案,我们只需要对图中的顶点进行排序,让它转换为一个线性序列,就可以解决问题,这时就需要用到一种叫拓扑排序的算法。

拓扑排序:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,此时就可以明确的表示出每个顶点的优先级。

下列是一幅拓扑排序后的示意图:

检测有向图中的环

如果学习x课程前必须先学习y课程,学习y课程前必须先学习z课程,学习z课程前必须先学习x课程,那么一定是有问题了,我们就没有办法学习了,因为这三个条件没有办法同时满足。其实这三门课程x、y、z的条件组成了一个环:

因此,如果我们要使用拓扑排序解决优先级问题,首先得保证图中没有环的存在,如果有环,那就无法进行拓扑排序

检测有向环的API设计

类名 DirectedCycle
构造方法 DirectedCycle(Digraph g):创建一个检测环对象,检测图G中是否有环
成员方法 private void dfs(Digraph g, int v):基于深度优先搜索,检测图G中是否有环
public boolean hasCycle():判断图中是否有环
成员变量 private boolean[] marked:索引代表顶点,值表示当前顶点是否已经被搜索
private boolean hasCycle:记录图中是否有环
private boolean[] onStack:索引代表顶点,使用栈的思想,记录当前顶点有没有已经处于正在搜索的有向路径上

检测有向环的代码实现

在API中添加了onStack[]布尔数组,索引为图的顶点,当我们深度搜索时:

  1. 在如果当前顶点正在搜索,则把对应的onStack数组中的值改为true,标识进栈;
  2. 如果当前顶点搜索完毕,则把对应的onStack数组中的值改为false,标识出栈;
  3. 如果即将要搜索某个顶点,但该顶点已经在栈中,则图中有环;

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 (var v = 0; v < g.v(); v++) {
            // 如果当前顶点还没有被搜索过,那么调用dfs搜索
            if (!marked[v]) {
                dfs(g, v);
            }
        }
    }

    /**
     * 基于深度优先搜索,检测图G中是否有环
     */
    private void dfs(Digraph g, int v) {
        // 标识当前顶点已被搜索
        marked[v] = true;
        // 把当前顶点进栈
        onStack[v] = true;

        // 进行深度搜索
        for (var w : g.adj(v)) {
            // 如果当前顶点w没有被搜索过,递归调用dfs搜索
            if (!marked[w]) {
                dfs(g, w);
            }
            // 如果当前顶点已经在栈中,表示有环
            if (onStack[w]) {
                hasCycle = true;
                return;
            }
        }

        // 把当前顶点出栈
        onStack[v] = false;
    }

    /**
     * 判断图中是否有环
     */
    public boolean hasCycle() {
        return hasCycle;
    }
    
}
复制代码

基于深度优先的顶点排序

如果要把图中的顶点生成线性序列其实是一件非常简单的事,之前我们学习并使用了多次深度优先搜索,我们会发现其实深度优先搜索有一个特点,那就是在一个连通子图上,每个顶点只会被搜索一次,如果我们能在深度优先搜索的基础上,添加一行代码,只需要将搜索的顶点放入到线性序列的数据结构中,我们就能完成这件事。

顶点排序API设计

类名 DepthFirstOrder
构造方法 DepthFirstOrder(Digraph g):创建一个顶点排序对象,生成顶点线性序列
成员方法 private void dfs(Digraph g, int v):基于深度优先搜索,生成顶点线性序列
public Stack<Integer> reversePost():获取顶点线性序列
成员变量 private boolean[] marked:索引代表顶点,值表示当前顶点是否已经被搜索
private Stack<Integer> reversePost:使用栈,存储顶点序列

顶点排序实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值