zoukankan      html  css  js  c++  java
  • 13_图

    为什么要有图

    为什么要有图

    前面我们看了线性表和树

    我们知道线性表局限于只有一个前驱和后继的关系

    树也只能有一个直接前趋,但是可以有多个后继

    但是这里有一些情况,假如我们需要使用多个直接前趋该怎么办呢?比如我们描述多对多关系的时候

    假如说现在有学生和老师这种情况,学生要有多个老师,老师也有多个学生

    当我们描述这种多对多关系,就需要图

    图的基本概述

    图的基本概述

    图是一种数据结构,其中一个节点可以有零个或者多个相邻的元素

    两个节点之间的连接称为边

    节点也可以称为顶点

    image-20210125120802636

    我们看上面的图,这个就是图,其中两个顶点的相连为边,比如BD

    图的常用概念

    1、顶点:也就是节点,上图的A、B、C、D、E

    2、边:两个节点的链接,比如AB

    3、路径:和无向图还是有向图有关,比如上图从D到C的路径可以为:D->B->C,或者D->A->B->D

    4、无向图:上图就是无向图,也就是顶点的连接没有方向,比如A和B,可以从A->B,也可以从B->A

    5、有向图:顶点之间有方向

    比如这里

    image-20210125121356423

    6、带权图:边带有权值,带权图也叫做网

    image-20210125121439603

    图的表示方式

    1、二维数组表示,我们称为邻接矩阵

    2、链表表示(或者数组+链表),我们称为邻接表

    邻接矩阵

    邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图来说,矩阵的row和col表示的是1...n个点

    image-20210125121722993

    在上图中,0代表不直接联通,1代表可直接联通

    比如0和0的值就是0,代表不联通,0和1之间是1,代表可联通

    但是有一些虽然不可以直接联通,但是是可以通过其他节点间接连通的

    邻接表

    邻接矩阵其实有个缺点,它为所有的顶点都分配了n个边的空间,其实这里面有很多空间都浪费掉了,所以为了避免资源的浪费,我们推出了邻接表,邻接表只关心存在的边,不关心不存在的边,所以不存在空间的浪费

    其实稀疏数组是可以对邻接矩阵进行优化的,但是转来转去太麻烦了,直接看邻接表

    image-20210125122345332

    在上图中,是使用数组+链表构成的,其中在数组索引中,0代表标号为0的顶点,1代表标号为1的顶点,....

    我们看标号为0的链表,举个例子。标号为0的链表上有1、2、3、4,这就代表顶点0可以和1、2、3、4直接相连

    再举个例子,看标号为1的链表,它有0、4,代表顶点1可以和顶点0、4直接相连

    注意,链表的节点不代表顺序,只代表和谁相连接


    图创建和代码实现

    思路分析

    image-20210126103017870

    代码实现以上结构:

    1、首先要存储这几个顶点,比如我们使用一个ArrayList来存储

    2、图的关系我们使用二维数组来保存这个矩阵int[][] edges,1代表能够直连,0代表不能直连

    代码实现

    package com.howling;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    
    public class GraphDemo {
        // 图中顶点的集合
        private ArrayList<String> vertexList;
    
        // 图中的顶点之间的连接关系,二维矩阵
        private int[][] edges;
    
        // 边的条数
        private int numOfEdges;
    
        /**
         * 构造器
         *
         * @param n 代表图中顶点的个数
         */
        public GraphDemo(int n) {
            // 初始化二维矩阵
            edges = new int[n][n];
    
            // 初始化顶点的ArrayList
            vertexList = new ArrayList<>(n);
    
            // 因为还不知道边的关系,所以初始化为0
            numOfEdges = 0;
        }
    
        /**
         * 插入节点
         *
         * @param vertex 向集合中插入节点
         */
        public void insertVertex(String vertex) {
            vertexList.add(vertex);
        }
    
        /**
         * 获取图中的顶点个数
         *
         * @return
         */
        public int getNumOfVertex() {
            return this.vertexList.size();
        }
    
        /**
         * 根据索引下标获取对应的值,索引取决于顶点添加的顺序
         *
         * @param index 对应的索引下标
         * @return
         */
        public String getValueByIndex(int index) {
            return vertexList.get(index);
        }
    
        /**
         * 添加两个顶点之间的边和边的权值
         * 假如两个顶点之间可以直接链接就是1,不能直接链接就是0
         *
         * @param v1     顶点1
         * @param v2     顶点2
         * @param weight 边的权值
         */
        public void insertEdge(int v1, int v2, int weight) {
    
            edges[v1][v2] = weight;
            edges[v2][v1] = weight;
            numOfEdges++;
        }
    
        /**
         * 获取边的个数
         *
         * @return 边的个数
         */
        public int getNumOfEdges() {
            return this.numOfEdges;
        }
    
        /**
         * 获取两个顶点之间,边的权值
         *
         * @param v1 顶点1
         * @param v2 顶点2
         * @return 权值
         */
        public int getWeight(int v1, int v2) {
            return edges[v1][v2];
        }
    
        public void showGraph() {
            Arrays.stream(edges).forEach(e -> System.out.println(Arrays.toString(e)));
        }
    
        public static void main(String[] args) {
            int n = 5;
            String[] VertexValue = {"A", "B", "C", "D", "E"};
    
            GraphDemo graph = new GraphDemo(n);
    
            // 添加顶点
            Arrays.stream(VertexValue).forEach(graph::insertVertex);
    
            // 添加边,A-B、A-C、B-C、B-D、B-E
            graph.insertEdge(0, 1, 1);
            graph.insertEdge(0, 2, 1);
            graph.insertEdge(1, 2, 1);
            graph.insertEdge(1, 3, 1);
            graph.insertEdge(1, 4, 1);
    
            graph.showGraph();
        }
    
    }
    

    图的遍历

    图的遍历介绍

    图的遍历,也就是对节点的访问。一个图中有许多节点,那么我们遍历这种节点需要一些策略

    一般来讲,有两种遍历策略:

    1、深度优先遍历

    2、广度优先遍历

    图的深度优先

    图的深度优先思想(Depth First Search)

    1、深度优先遍历,也就是从初始访问节点出发,初始访问节点可能有多个邻接节点

    深度优先遍历的策略是首先访问第一个节点,再以这个被访问的邻接节点作为初始访问节点,继续下一次访问

    2、我们可以看到,这种访问策略是优先向纵深方向,而不是对一个节点的所有邻接节点进行的横向访问

    3、显然,深度优先是一个递归的过程

    深度优先的算法步骤

    1、访问初始节点v,并标记v节点已访问
    2、查找v的第一个邻接节点w
    	2.1、假如w不存在(不直接连通),那么返回第一步,从v的下一个节点继续
    	2.2、假如w存在(直接连通)
    		2.2.1、假如w未被访问,则标记w已访问,并以w作为初始节点,进行第一步
    		2.2.2、假如w已经被访问,则查找节点v的其他邻接节点,转向第二步
    

    深度优先举例

    image-20210126115438890

    我们以上图来举例子,假如我现在从顶点A开始进行深度优先遍历

    那么我首先规定,这几个节点的相邻顺序为:A->B->C->D->E

    1、A为开始节点,标记为已遍历

    2、A找到相邻节点B,发现与B不能直接连通,继续寻找下一个节点

    3、A找到相邻节点C,发现可以与C直接连通,则进入C并标记C为已遍历

    4、C找到相邻节点A,发现可以连通但是已遍历,继续寻找下一个节点

    5、C找到相邻节点B,发现可以连通并且没有遍历,则进入B并标记B为已遍历

    6、B寻找到A和C,发现A不可连通,C已经遍历,继续寻找下一个节点

    7、B寻找D,发现D满足条件,则进入D并标记D为已遍历

    8、D分别找到了A、B、C、E,发现均不满足条件,则返回上一层B

    9、B继续寻找到E,发现E满足条件,进入到E并标记E为已遍历

    10、E寻找到了A、B、C、D,发现均不满足条件,有没有其他节点,所以推出到上一层B

    11、B发现没有其他节点,推出到上一层C

    12、C继续寻找D、E,发现均不满足条件,没有了其他节点,退出到A

    13、A继续寻找D、E,发现均不满足条件,没有其他节点,结束递归

    图的深度优先代码实现

    image-20210126115438890

    我们按照这张图来进行一次深度优先

    package com.howling;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    
    public class GraphDemo {
        // 图中顶点的集合
        private ArrayList<String> vertexList;
    
        // 图中的顶点之间的连接关系,二维矩阵
        private int[][] edges;
    
        // 边的条数
        private int numOfEdges;
    
        // 定义boolean数组,标记某个节点是否被访问
        private boolean[] isVisited;
    
    
        /**
         * 构造器
         *
         * @param n 代表图中顶点的个数
         */
        public GraphDemo(int n) {
            // 初始化二维矩阵
            edges = new int[n][n];
    
            // 初始化顶点的ArrayList
            vertexList = new ArrayList<>(n);
    
            // 因为还不知道边的关系,所以初始化为0
            numOfEdges = 0;
    
            // 记录某个顶点是否被访问
            isVisited = new boolean[n];
        }
    
        /**
         * 返回没有遍历过的相邻节点的下标
         *
         * @param index 当前节点的下标
         * @return 邻接节点的下标
         */
        public int getNeighbor(int index) {
            // 首先把自己标记为已经遍历
            isVisited[index] = true;
    
            // 从头开始遍历,找到能够连接并且没有标记为已遍历的节点
            for (int i = 0; i < vertexList.size(); i++) {
                if (i == index) {
                    continue;
                }
                // 假如找到一个能够连通的,并且没有遍历过的,那么就标记此节点已经遍历并返回对应的坐标
                if (edges[index][i] > 0 && !isVisited[i]) {
                    return i;
                }
            }
    
            // 假如找不到符合要求的节点,返回-1
            return -1;
        }
    
        /**
         * 图的深度优先遍历
         *
         * @param index 当前节点
         */
        public void DFS(int index) {
            // 首先输出当前节点
            System.out.println(vertexList.get(index));
    
            // 找到下一个相邻的、可以直接连通、没有遍历过的节点,递归
            while (true) {
                int neighbor = getNeighbor(index);
                if (neighbor == -1) {
                    return;
                }
                DFS(neighbor);
            }
        }
    
        public static void main(String[] args) {
            int n = 5;
            String[] VertexValue = {"A", "B", "C", "D", "E"};
    
            GraphDemo graph = new GraphDemo(n);
    
            // 添加顶点
            Arrays.stream(VertexValue).forEach(graph::insertVertex);
    
            // 添加边,A-C、B-C、B-D、B-E、C-D
            graph.insertEdge(0, 2, 1);
            graph.insertEdge(1, 2, 1);
            graph.insertEdge(1, 3, 1);
            graph.insertEdge(1, 4, 1);
            graph.insertEdge(2, 3, 1);
    
            // 关系图
            graph.showGraph();
    
            System.out.println("-----------------------");
    
            // 深度优先遍历,顺序应该为:A-->C-->B-->D-->E
            graph.DFS(0);
    
        }
    
    }
    

    因为东西太多,所以只放出部分,剩下的在之前都有实现


    图的广度优先

    图的广度优先思想(Broad First Search)

    广度优先类似一个分层搜索的功能,需要使用一个队列以保持访问过的节点的顺序。

    简单来说,比如说从A开始访问,那么我首先走一遍能和A进行互通的所有节点入队列。

    等到A所互通的节点走完之后,就把A从队列里面剔除,从队列的头开始再次进行遍历,遍历到之后就入队列,循环往复。

    这个时候,只要队列不为空,就说明还没有结束,假如队列为空,则代表全部都已经遍历完了

    广度优先遍历算法步骤

    1、访问初始节点v,将v标记为已经访问

    2、节点v入队列,当队列非空时,继续执行,否则算法结束

    3、查找v的相邻节点,全部入队列,并标记为已经访问

    4、v的相邻节点全部遍历完成,则v出队列

    5、根据队列中的第一个元素u进行查找相邻节点,循环往复

    6、当队列为空时,算法结束

    这个广度优先还不算很难,所以不举例子了

    广度优先代码实现

    image-20210126115438890

    按照广度优先遍历,这个最终输出的顺序应该为:A-->C-->B-->D-->E

    package com.howling;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.LinkedList;
    
    public class GraphDemo {
        // 图中顶点的集合
        private ArrayList<String> vertexList;
    
        // 图中的顶点之间的连接关系,二维矩阵
        private int[][] edges;
    
        // 边的条数
        private int numOfEdges;
    
        // 定义boolean数组,标记某个节点是否被访问
        private boolean[] isVisited;
    
    
        /**
         * 构造器
         *
         * @param n 代表图中顶点的个数
         */
        public GraphDemo(int n) {
            // 初始化二维矩阵
            edges = new int[n][n];
    
            // 初始化顶点的ArrayList
            vertexList = new ArrayList<>(n);
    
            // 因为还不知道边的关系,所以初始化为0
            numOfEdges = 0;
    
            // 记录某个顶点是否被访问
            isVisited = new boolean[n];
        }
        
        /**
         * 返回没有遍历过的相邻节点的下标
         *
         * @param index 当前节点的下标
         * @return 邻接节点的下标
         */
        public int getNeighbor(int index) {
            // 首先把自己标记为已经遍历
            isVisited[index] = true;
    
            // 从头开始遍历,找到能够连接并且没有标记为已遍历的节点
            for (int i = 0; i < vertexList.size(); i++) {
                if (i == index) {
                    continue;
                }
                // 假如找到一个能够连通的,并且没有遍历过的,那么就标记此节点已经遍历并返回对应的坐标
                if (edges[index][i] > 0 && !isVisited[i]) {
                    isVisited[i] = true;
                    return i;
                }
            }
    
            // 假如找不到符合要求的节点,返回-1
            return -1;
        }
    
        /**
         * 图的广度优先遍历
         *
         * @param index 开始的下标
         */
        public void BFS(int index) {
    
            // 队列
            LinkedList<Integer> queue = new LinkedList<>();
    
            // 首先将初始节点加入到队列中
            queue.add(index);
    
            while (!queue.isEmpty()) {
                // 查找下一个相邻的、可以直接连通的、没有访问过的节点
                int neighbor = getNeighbor(queue.get(0));
                // 假如可以找到符合要求的节点
                if (neighbor != -1) {
                    // 将指定的节点加入到队列中
                    queue.add(neighbor);
                }
                // 假如可以找到符合要求的节点,那么队列首部出队列并且打印
                else {
                    Integer first = queue.removeFirst();
                    System.out.print(vertexList.get(first) + "  ");
                }
            }
    
        }
    
        public static void main(String[] args) {
            int n = 5;
            String[] VertexValue = {"A", "B", "C", "D", "E"};
    
            GraphDemo graph = new GraphDemo(n);
    
            // 添加顶点
            Arrays.stream(VertexValue).forEach(graph::insertVertex);
    
            // 添加边,A-C、B-C、B-D、B-E、C-D
            graph.insertEdge(0, 2, 1);
            graph.insertEdge(1, 2, 1);
            graph.insertEdge(1, 3, 1);
            graph.insertEdge(1, 4, 1);
            graph.insertEdge(2, 3, 1);
    
            // 关系图
            graph.showGraph();
    
            System.out.println("-----------------------");
    
            // 深度优先遍历,顺序应该为:A-->C-->B-->D-->E
            graph.BFS(0);
    
        }
    }
    

    这里小小地修改了getNeighbor,在返回节点之前也进行了一次的改变遍历的状态


    广度优先和深度优先比较

    image-20210126155051342

    在上图中:

    1、深度优先:1-->2-->4-->8-->5-->3-->6-->7

    2、广度优先:1-->2-->3-->4-->5-->6-->7-->8

  • 相关阅读:
    Redis 3.0 与 3.2 配置文件变化
    PHP 位运算(&, |, ^, ~, <<, >>)及 PHP错误级别报告设置(error_reporting) 详解
    MySQL自增ID 起始值 修改方法
    CentOS 6.5 编译 PHP-7 报错:undefined reference to `libiconv_open 无法编译 PHP libiconv
    file xxx from install of xxx conflicts with file from xxx
    专家访谈 / 架构分享 / 网摘 收藏
    怎样用javascript获取UUID
    ansible经常使用模块使用方法
    一个button导致的慘案
    nil coalescing operator
  • 原文地址:https://www.cnblogs.com/howling/p/14331092.html
Copyright © 2011-2022 走看看