有向图-java语言版

847 阅读2分钟

-- 有向图

1、有向图是一副具有方向性的图,是由一组顶点和一组有方向的边组成的,每条方向的边都连着一对有序的顶点。

有向图.png

有向图相关术语:
    1、出度: 由某个顶点指出的边的个数称为该顶点的出度。
    2、入度: 指向某个顶点的边的个数称为该顶点的入度。
    3、有向路径: 由一系列顶点组成,对于其中的每个顶点都存在一条有向边,从它指向序列中的下一个顶点。
    4、有向环: 一条至少含有一条边,且起点和终点相同的有向路径。

-- API:

有向图api.png

-- 代码:

/**
 * 有向图
 */
public class Digraph {
    // 顶点数目
    private final int V;
    // 边数目
    private int E;
    // 邻接表
    private Queue<Integer>[] adj;

    // 构造方法
    public Digraph(int v) {
        // 初始化顶点数目
        V = v;
        // 初始化边数目
        E = 0;
        // 初始化邻接表
        adj = new Queue[v];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<Integer>();
        }
    }

    // 获取顶点数量
    public int V() {return V;}

    // 获取边数量
    public int E() {return E;}

    // 往图中添加一条边v-w
    public void addEdge(int v,int w) {
        // 将w添加到v邻接表
        adj[v].enqueue(w);
        // 边数量+1
        E++;
    }

    // 获取某个顶点的邻接表
    public Queue<Integer> adj(int v) {return adj[v];}

    // 获取该图的反向图
    public Digraph reverse() {
        // 创建一个图保存反向图
        Digraph digraph = new Digraph(V);
        // 遍历邻接表,构建反向图
        for (int v = 0; v < V; v++) {
            for (Integer w : adj(v)) {
                digraph.addEdge(w,v);
            }
        }
        return digraph;
    }
}

-- 拓扑排序

**拓扑排序的目的是:将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素**

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

拓扑排序1.png

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

拓扑排序2.png

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

-- 检测有向图中的环

 拓扑排序的目的是:给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素
 
 有向图中有环则不能进行拓扑排序,所有进行拓扑排序前要检查图中是否有环
 

-- api:

检测是否有环.png

-- 代码:

/**
 * 检查有向图是否有环
 */
public class DirectedCycle {
    // 记录顶点是否已被搜索
    private boolean[] marked;
    // 记录是否有顶点
    private boolean hasCycle;
    // 记录顶点是否在正在搜索队列
    private boolean[] onStack;

    // 构造方法
    public DirectedCycle(Digraph digraph) {
        marked = new boolean[digraph.V()];
        hasCycle = false;
        onStack = new boolean[digraph.V()];
        // 遍历所有顶点作为入口进行深度搜索
        for (int v = 0; v < digraph.V(); v++) {
            // 当前顶点未被搜索,则进行深度搜索
            if(!marked[v]) {
                dfs(digraph,v);
            }
        }
    }

    // 深度优先搜索,检查是否有环
    private void dfs(Digraph digraph,int v) {
        // 当前顶点标记为已搜索
        marked[v] = true;
        // 当顶点加入正在搜索队列
        onStack[v] = true;

        // 遍历当前顶点邻接表
        for (Integer w : digraph.adj(v)) {
            // 如果邻接表中的w顶点,未被搜索则递归进行搜索
            if(!marked[w]) {
                dfs(digraph,w);
            }

            // 检查当前顶点是否在搜索队列中,除非有顶点被搜索第二次,不会不会执行到这里
            if(onStack[w]) {
                hasCycle = true;    // 标记为有环
                return;
            }
        }
        // 递归返回时清空搜索队列
        onStack[v] = false;
    }

    // 当前顶点是否有环
    public boolean hasCycle() {
        return hasCycle;
    }
}

-- 顶点排序

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

在API的设计中,我们添加了一个栈reversePost用来存储顶点,当我们深度搜索图时,每搜索完毕一个顶点,把该 
顶点放入到reversePost中,这样就可以实现顶点排序。

顶点排序.png

-- 代码:

/**
 * 基于深度优先搜索的顶点排序
 */
public class DepthFirstOrder {
    // 记录当前顶点是否被搜索
    private boolean[] marked;
    // 使用栈,保存顶点序列
    private Stack<Integer> reversePost;

    // 构造方法
    public DepthFirstOrder(Digraph digraph) {
        // 初始化marked
        marked = new boolean[digraph.V()];
        // 初始化栈
        reversePost = new Stack<Integer>();

        // 遍历所有顶点
        for (int v = 0; v < digraph.V(); v++) {
            // 如果当前顶点未被搜索,则进行深度搜索
            if(!marked[v]) {
                dfs(digraph,v);
            }
        }
    }

    // 使用深度优先搜索,进行顶点排序
    private void dfs(Digraph digraph,int v) {
        // 标记当前顶点,未已搜索
        marked[v] = true;
        // 遍历当前顶点邻接表
        for (Integer w : digraph.adj(v)) {
            // 如果当前顶点未被搜索,则递归继续搜索
            if(!marked[w]) {
                dfs(digraph,w);
            }
        }

        // 递归返回时,将顶点加入栈中
        reversePost.push(v);
    }

    // 获取顶点线性序列
    public Stack<Integer> reversePost() {
        return reversePost;
    }
}

-- 拓扑排序

前面已经实现了环的检测以及顶点排序,那么拓扑排序就很简单了,基于一幅图,先检测有没有环,如果没有环, 
则调用顶点排序即可。

-- api:

拓扑排序.png

-- 代码:

/**
 * 拓扑排序
 */
public class TopoLogical {
    // 顶点的拓扑排序
    private Stack<Integer> order;

    // 构造方法
    public TopoLogical(Digraph digraph) {
        // 创建检查环对象
        DirectedCycle directedCycle = new DirectedCycle(digraph);
        // 如果不存在环,则进行顶点排序
        if(!directedCycle.hasCycle()) {
            // 创建顶点排序对象
            DepthFirstOrder depthFirstOrder = new DepthFirstOrder(digraph);
            // 返回顶点排序序列
            order = depthFirstOrder.reversePost();
        }
    }

    // 判断顶点是否有环
    public boolean idCycle() {
        return order == null;
    }

    // 获取拓扑排序的所有顶点
    public Stack<Integer> order() {
        return order;
    }
}

-- 测试代码:

/**
 * 测试拓扑排序
 */
public class OrderTest {
    @Test
    public void orderTest() {
        // 创建一个无向图
        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 = new TopoLogical(digraph);
        Stack<Integer> order = topoLogical.order();

        for (Integer w : order) {
            System.out.println(w);
        }
    }
}

-- 运行结果:

拓扑排序效果图.png

@ 以上内容属于个人笔记