20210419-算法学习-图(Graph)-深度优先,广度优先

一.图的基本介绍

  1.图的基本介绍(为什么要有图?)

    1)之前我们学习了线性表和树,但线性表局限于一个直接前驱和一个直接后继的关系,树也只能有一个直接前驱(也就是父节点),当我们需要表示多对多的关系时,这里就用到了图来表达

  2.如何理解 "图"

    1)图(Graph)是一种数据结构,和树比起来,这是一种更加复杂的非线性表结构如下图所示,图中的顶点可以与任意其他顶点建立链接关系,这种建立的关系叫做边(edge)

      

    2)举个例子1:微信好友关系

*比如在微信中可以把每个用户看作一个顶点
*如果两个用户之间互加好友,那就在两者之间建立一条边
*所以,整个微信的好友关系就可以用一张图来表示。其中,每个用户有多少个好友,对应到图中,就叫作顶点的度(degree),就是跟顶点相连接的边的条数

3)举个例子2:微博,微博社交关系跟微信有点不一样,或者说更加复杂一点

  *微博允许单向关注,也就是说,用户 A 关注了用户 B,但用户 B 可以不关注用户 A
  *可以把图结构稍微改造一下,引入边的“方向”的概念
  *如果用户 A 关注了用户 B,就在图中画一条从 A 到 B 的带箭头的边,来表示边的方向
  *如果用户 A 和用户 B 互相关注了,那就画一条从 A 指向 B 的边,再画一条从 B 指向 A 的边,这就引出"方向"的概念

以此类推,反之不带方向的叫做"无向图"

  

  *无向图中有“度”这个概念,表示一个顶点有多少条边。在有向图中,把度分为入度(In-degree)和出度(Out-degree)
  *顶点的入度,表示有多少条边指向这个顶点;顶点的出度,表示有多少条边是以这个顶点为起点指向其他顶点
  *对应到微博的例子,入度就表示有多少粉丝,出度就表示关注了多少人

    4)举个例子3:QQ,微博社交关系跟微信有点不一样,或者说更加复杂一点,QQ 中的社交关系要更复杂的一点。QQ 不仅记录了用户之间的好友关系,还记录了两个用户之间的亲密度

      *如果两个用户经常往来,那亲密度就比较高;如果不经常往来,亲密度就比较低
      *要用到另一种图, 带权图(weighted graph)
      *在带权图中,每条边都有一个权重(weight),可以通过这个权重来表示 QQ 好友间的亲密度

      

  3.图的常用概念及术语

    1)顶点(vertex)

    2)边(edge)

    3)路径

    4)无向图

      

    5)有向图

      

    6)带权图

      

二.图的表示方式

 

  1.图的表示方式有两种:

    1)二维数组表示(邻接矩阵)

    2)链表 表示(邻接表) 

  2.邻接矩阵:

    1)邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵中的 row(横) 和 col(竖) 表示的是 1... ..n 个节点    

       

    2)邻接矩阵存储方法

      *图最直观的一种存储方法就是, 邻接矩阵(Adjacency Matrix)
      *邻接矩阵的底层依赖一个二维数组
      *对于无向图来说,如果顶点 i 与顶点 j 之间有边,就将 A[i][j] 和 A[j][i] 标记为 1
      *对于有向图来说,如果顶点 i 到顶点 j 之间有一条箭头从顶点 i 指向顶点 j 的边,那就将 A[i][j] 标记为 1
      *同理,如果有一条箭头从顶点 j 指向顶点 i 的边,就将 A[j][i] 标记为 1

      *对于带权图,数组中就存储相应的权重

      

 

      *1)用邻接矩阵来表示一个图,虽然简单、直观,但是比较浪费存储空间:
        *因为对于无向图来说,如果 A[i][j] 等于 1,那 A[j][i] 也肯定等于 1。实际上,只需要存储一个就可以了
        *也就是说,无向图的二维数组中,如果将其用对角线划分为上下两部分,那只需要利用上面或者下面这样一半的空间就足够了,另外一半白白浪费掉了
        *还有,如果存储的是稀疏图(Sparse Matrix),也就是说,顶点很多,但每个顶点的边并不多,那邻接矩阵的存储方法就更加浪费空间了
        *比如微信有好几亿的用户,对应到图上就是好几亿的顶点。但是每个用户的好友并不会很多,一般也就三五百个而已。如果用邻接矩阵来存储,那绝大部分的存储空间都被浪费了

 

      *2)邻接矩阵的存储方法的优点:

        *首先,邻接矩阵的存储方式简单、直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效
        *其次,用邻接矩阵存储图的另外一个好处是方便计算。这是因为,用邻接矩阵的方式存储图,可以将很多图的运算转换成矩阵之间的运算  

  3.邻接表

    1)邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在的,或造成空间的一定损失

    2)邻接表的实现,只关心存在的边,不关心不存在的边,因此没有空间浪费,邻接表是由数组+链表组成的

    3)举例说明:

      

    4)邻接表存储方式

      *针对上面邻接矩阵比较浪费内存空间的问题,来看另外一种图的存储方法,邻接表(Adjacency List)
      *邻接表有点像散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点
      *图中画的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是指向的顶点
      *对于无向图来说,也是类似的,不过,每个顶点的链表中存储的,是跟这个顶点有边相连的顶点

      

 

 

      *1)这其实就是时间、空间复杂度互换的设计思想
        *邻接矩阵存储起来比较浪费空间,但是使用起来比较节省时间。相反,邻接表存储起来比较节省空间,但是使用起来就比较耗时间
        *就像图中的例子,如果要确定,是否存在一条从顶点 2 到顶点 4 的边,那就要遍历顶点 2 对应的那条链表,看链表中是否存在顶点 4
        *但是,链表的存储方式对缓存不友好,所以,比起邻接矩阵的存储方式,在邻接表中查询两个顶点之间的关系就没那么高效了

      *在基于链表法解决冲突的散列表中,如果链过长,为了提高查找效率,可以将链表换成其他更加高效的数据结构,比如平衡二叉查找树等
      *邻接表长得很像散列表。所以,也可以将邻接表同散列表一样进行“改进升级”
      *可以将邻接表中的链表改成平衡二叉查找树,来提高查询效率
      *实际开发中,可以选择用红黑树。这样,就可以更加快速地查找两个顶点之间是否存在边了
      *这里的二叉查找树可以换成其他动态数据结构,比如跳表、散列表等
      *除此之外,还可以将链表改成有序动态数组,可以通过二分查找的方法来快速定位两个顶点之间否是存在边

 

三.图的快速入门案例

  1.要求代码实现如下结构

    

  2.思路分析

    1)存储顶点String 使用 ArrayList

    2)保存矩阵 int[] edges

  3.代码实现

//存储节点的集合
    private ArrayList<String> vertexList;
    //存储图对应的邻接矩阵
    private int[][] edges;
    //表示边的数目
    private int numOfEdges;

    //插入节点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    /**
     * 添加边
     *
     * @return void
     * @Author lisongtao
     * @Description :
     * @Date 2021/4/19 14:36
     * @Param [v1 表示点的下标是第几个顶点 "A" - "B" "A" ->0 "B" -> 1, v2 表示第二个顶点对应的下标, weight 表示权值]
     **/
    public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

四.图的深度优先遍历介绍 

  1.图遍历介绍:所谓图的遍历,即是对节点的访问,一个图有那么多个节点,如何遍历这些节点,需要特定策略,一般有两种访问策略

    1)深度优先遍历

    2)广度优先遍历

  2.深度优先遍历基本思想(图的深度优先搜索-Depth First Search)

    1)深度优先遍历,从初始访问节点出发,初始访问节点可能有多个邻接节点,深度优先遍历的策略就是首先访问第一个邻接节点,然后在以这个被访问的邻接节点作为初始节点,访问他的第一个邻接节点,也可以这样理解:

  • 每次都在访问完当前节点后首先访问当前节点的第一个邻接节点

 

    2)可以看到这样的访问策略是优先往纵向挖掘深入,而不是对一个节点的所有邻接节点进行横向访问

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

  3.深度优先遍历算法步骤

    1)访问初始节点v,并标记节点v为 已访问

    2)查找节点v的第一个邻接节点w

    3)若w存在,则继续执行4,如果w不存在,则返回到第1步骤,将从v的下一个节点继续

    4)若w未被访问,对w进行深度优先遍历递归,(即把w当做另一个v,然后进行步骤1,2,3)

    5)查找节点v的w邻接节点的下一个邻接节点,转到步骤3

    6)分析图:

      

  4.深度优先算法的代码实现

 //定义给数组 boolean[], 记录某个结点是否被访问
    private boolean[] isVisited;


//核心代码
    //深度优先遍历算法
    // i 第一次就是0
    private void dfs(boolean[] isVisited, int i) {
        //首先我们访问该节点,输出
        System.out.println(getValueByIndex(i) + "->");
        //将节点设置为已经访问
        isVisited[i] = true;
        //查找节点 i的第一个邻接节点w
        int w = getFirstNeighbor(i);
        while (w != -1) {//说明有
            if (!isVisited[w]) {
                dfs(isVisited, w);
            }
            //如果 w节点已经被访问过
            w = getNextNeighbor(i, w);
        }
    }

    //对 dfs进行一次重载,遍历我们所有的节点,并进行 dfs
    public void dfs() {
        isVisited = new boolean[vertexList.size()];
        //遍历所有的节点,进行dfs[回溯]
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                dfs(isVisited, i);
            }
        }
    }
	
	 //返回节点 i(下标)对应的数据 0->"A" 1->"B" 2->"C"
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    /**
     * 得到第一个邻接结点的下标 w
     *
     * @param index
     * @return 如果存在就返回对应的下标,否则返回-1
     */
    public int getFirstNeighbor(int index) {
        for (int i = 0; i < vertexList.size(); i++) {
            if (edges[index][i] > 0) {
                return i;
            }
        }
        return -1;
    }

    //根据前一个邻接结点的下标来获取下一个邻接结点
    public int getNextNeighbor(int v1, int v2) {
        for (int i = v2 + 1; i < vertexList.size(); i++) {
            if (edges[v1][i] > 0) {
                return i;
            }
        }
        return -1;
    }

    //图中常用的方法
    //返回结点的个数
    public int getNumOfVertex() {
        return vertexList.size();
    }

四.图的广度优先遍历介绍  

  1.广度优先遍历基本思想(Broad First Search)

    1)类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的节点的顺序,以便按照这个顺序来访问这些结构的邻接节点

  2.广度优先遍历算法步骤

    1)访问初始节点 v并标记节点 v为已访问

    2)节点 v入队列

    3)当队列非空时,继续执行,否则算法结束

    4)出队列,取得队列头节点u

    5)查找节点 u的第一个邻接节点 w

    6)若节点 u的邻接节点 w不存在,则转到步骤3,否则循环执行以下三个步骤:

  • 若节点 w尚未被访问,则访问节点w 并标记为已访问
  • 节点 w入列
  • 查找节点 u的 既w 的邻接节点后的下一个邻接节点 w,转到步骤6

  3.广度优先算法示意图:

    

  4.广度优先算法的代码实现

//对一个节点进行广度优先遍历的方法
    private void bfs(boolean[] isVisited, int i) {
        int u;//表示队列的头节点对应的下标
        int w;//邻接节点w
        //队列,记录节点访问的顺序
        LinkedList queue = new LinkedList();
        //访问节点,输出节点信息
        System.out.println(getValueByIndex(i) + "=>");
        //标记已访问
        isVisited[i] = true;
        //将节点加入队列
        queue.addLast(i);

        while (!queue.isEmpty()) {
            //取出队列的头节点的下标
            u = (Integer) queue.removeFirst();
            //得到第一个邻接节点的下标w
            w = getFirstNeighbor(u);

            while (w != -1) {//找到
                //是否访问过
                if (!isVisited[w]) {
                    System.out.println(getValueByIndex(w) + "=>");
                    //标记已经访问
                    isVisited[w] = true;
                    //入队
                    queue.addLast(w);
                }
                //以 u为前驱点,找 w后面的下一个邻接节点
                w = getNextNeighbor(u, w);
            }
        }
    }

    //遍历所有的节点,都进行广度优先搜索
    public void bfs() {
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                bfs(isVisited, i);
            }
        }
    }

五.图的代码汇总

package com.atAlgorithmTest;
/**
 * @Author: lisongtao
 * @Date: 2021/4/19 14:13
 */


import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;

/**
 * @ClassName Graph
 * @Description : 图的应用案例
 * @Author DELL
 * @Date 2021/04/19 14:13
 **/
public class Graph {
    //存储节点的集合
    private ArrayList<String> vertexList;
    //存储图对应的邻接矩阵
    private int[][] edges;
    //表示边的数目
    private int numOfEdges;
    //定义给数组 boolean[], 记录某个结点是否被访问
    private boolean[] isVisited;

    //插入节点
    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    /**
     * 添加边
     *
     * @return void
     * @Author lisongtao
     * @Description :
     * @Date 2021/4/19 14:36
     * @Param [v1 表示点的下标是第几个顶点 "A" - "B" "A" ->0 "B" -> 1, v2 表示第二个顶点对应的下标, weight 表示权值]
     **/
    public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

    //==============================深度优先算法的代码实现==============================

    //核心代码
    //深度优先遍历算法
    // i 第一次就是0
    private void dfs(boolean[] isVisited, int i) {
        //首先我们访问该节点,输出
        System.out.println(getValueByIndex(i) + "->");
        //将节点设置为已经访问
        isVisited[i] = true;
        //查找节点 i的第一个邻接节点w
        int w = getFirstNeighbor(i);
        while (w != -1) {//说明有
            if (!isVisited[w]) {
                dfs(isVisited, w);
            }
            //如果 w节点已经被访问过
            w = getNextNeighbor(i, w);
        }
    }

    //对 dfs进行一次重载,遍历我们所有的节点,并进行 dfs
    public void dfs() {
        isVisited = new boolean[vertexList.size()];
        //遍历所有的节点,进行dfs[回溯]
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                dfs(isVisited, i);
            }
        }
    }

    //返回节点 i(下标)对应的数据 0->"A" 1->"B" 2->"C"
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    //构造器
    public Graph(int n) {
        //初始化矩阵和 vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdges = 0;
    }

    /**
     * 得到第一个邻接结点的下标 w
     *
     * @param index
     * @return 如果存在就返回对应的下标,否则返回-1
     */
    public int getFirstNeighbor(int index) {
        for (int i = 0; i < vertexList.size(); i++) {
            if (edges[index][i] > 0) {
                return i;
            }
        }
        return -1;
    }

    //根据前一个邻接结点的下标来获取下一个邻接结点
    public int getNextNeighbor(int v1, int v2) {
        for (int i = v2 + 1; i < vertexList.size(); i++) {
            if (edges[v1][i] > 0) {
                return i;
            }
        }
        return -1;
    }

    //图中常用的方法
    //返回结点的个数
    public int getNumOfVertex() {
        return vertexList.size();
    }

    //显示图对应的矩阵
    public void showGraph() {
        for (int[] link : edges) {
            System.err.println(Arrays.toString(link));
        }
    }


    //==============================广度优先算法的代码实现==============================

    //对一个节点进行广度优先遍历的方法
    private void bfs(boolean[] isVisited, int i) {
        int u;//表示队列的头节点对应的下标
        int w;//邻接节点w
        //队列,记录节点访问的顺序
        LinkedList queue = new LinkedList();
        //访问节点,输出节点信息
        System.out.println(getValueByIndex(i) + "=>");
        //标记已访问
        isVisited[i] = true;
        //将节点加入队列
        queue.addLast(i);

        while (!queue.isEmpty()) {
            //取出队列的头节点的下标
            u = (Integer) queue.removeFirst();
            //得到第一个邻接节点的下标w
            w = getFirstNeighbor(u);

            while (w != -1) {//找到
                //是否访问过
                if (!isVisited[w]) {
                    System.out.println(getValueByIndex(w) + "=>");
                    //标记已经访问
                    isVisited[w] = true;
                    //入队
                    queue.addLast(w);
                }
                //以 u为前驱点,找 w后面的下一个邻接节点
                w = getNextNeighbor(u, w);
            }
        }
    }

    //遍历所有的节点,都进行广度优先搜索
    public void bfs() {
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                bfs(isVisited, i);
            }
        }
    }


    public static void main(String[] args) {
        //测试一把图是否创建 ok
        int n = 8; //结点的个数
        //String Vertexs[] = {"A", "B", "C", "D", "E"};
        String Vertexs[] = {
                "1", "2", "3", "4", "5", "6", "7", "8"
        };
        //创建图对象
        Graph graph = new Graph(n);
        //循环的添加顶点
        for (String vertex : Vertexs) {
            graph.insertVertex(vertex);
        }
        //添加边
        //A-B A-C B-C B-D B-E
//        graph.insertEdge(0, 1, 1); // A-B
//        graph.insertEdge(0, 2, 1); //
//        graph.insertEdge(1, 2, 1); //
//        graph.insertEdge(1, 3, 1); //
//        graph.insertEdge(1, 4, 1); //
        //
        //
        //
        //
        //
        //更新边的关系
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);
        graph.insertEdge(3, 7, 1);
        graph.insertEdge(4, 7, 1);
        graph.insertEdge(2, 5, 1);
        graph.insertEdge(2, 6, 1);
        graph.insertEdge(5, 6, 1);

        //显示一把邻结矩阵
        graph.showGraph();
        //测试一把,我们的 dfs 遍历是否 ok
        System.out.println("深度遍历");
        graph.dfs(); // A->B->C->D->E [1->2->4->8->5->3->6->7]
        System.out.println();
        //
        System.out.println("广度优先!");
        graph.bfs(); // A->B->C->D-E [1->2->3->4->5->6->7->8]
    }
}

  

  

 

 

  

    

 

posted @ 2021-04-19 09:33  firefox7557  阅读(357)  评论(0)    收藏  举报