zoukankan      html  css  js  c++  java
  • 【数据结构】——图到底是个什么东西?

    不要你觉得,我要我觉得,我说图它不是个东西。——明人明言。

    为什么有图

    用来表示多对多的关系。

    线性表局限于一个直接前驱和一个直接后继的关系

    树也只能有一个直接前驱也就是父节点

    基本概念

    边:两结点的连线

    顶点(vertex):数据元素,一个顶点可以具有零个或多个相邻元素。

    路径: 比如从 D-> C 的路径有

    1)D->B->C

    2)D->A->B->C

    分类

    无向图:如上图,顶点间连线无方向。比如A-B,即可以是 A-> B 也可以 B->A .

    有向图:顶点之间的连接有方向,比如A-B,

    只能是 A-> B 不能是 B->A

    带权图:这种边带权值的图也叫网

    表示方式

    或者也就存储结构

    图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。

    邻接矩阵

    我们用两个数组来表示图:

    一维数组用来存储图中顶点信息

    二维数组存放存放图中边信息

    求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arr[i][j]为1的就是邻接点。

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

    下面是无向图的一个例子,观察:

    邻接矩阵是对称矩阵

    主对角线为0,不存在顶点到自身的边;

    邻接表

    只关心存在的边。

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

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

    创建一个图

    代码实现如下图结构

        A   B   C   D   E
    A   0   1   1   0   0
    B   1   0   1   1   1
    C   1   1   0   0   0
    D   0   1   0   0   0 
    E   0   1   0   0   0
    //说明
    //(1) 1 表示能够直接连接
    //(2) 0 表示不能直接连接
    

    思路分析:

    需要两个数组

    String类型的ArrayList用来存储顶点

    二维数组来保存边信息

    常用方法:

    1. 插入顶点
    2. 插入边
    3. 返回结点的个数
    4. 得到边的数目,每插入边就累加一次
    5. 显示图对应的矩阵
    6. 返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
    7. 返回v1和v2的权值,该权值存在数组里。

    代码实现

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.LinkedList;
    
    /**
     * @ClassName: Demo20_Graph
     * @author: benjamin
     * @version: 1.0
     * @description: TODO
     * @createTime: 2019/08/26/11:20
     */
    
    public class Demo20_Graph {
    
      // 用于存储顶点的集合
      private ArrayList<String> vertexList;
      // 存储边的信息的二维数组
      private int[][] edges;
      // 记录边的数目
      private int numOfEdges;
      // 定义数组boolean[],记录某个结点是否被访问
    
      public static void main(String[] args) {
        Demo20_Graph graph = new Demo20_Graph(5);
    
        // 插入顶点
        String vertexs[] = {"A","B","C","D","E","F"};
        for(String vertex:vertexs){
            graph.insertVertex(vertex);
        }
        // 添加边
        // 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();
      }
    
      // 构造器,初始化矩阵和顶点集合
      Demo20_Graph(int n) {
        // 集合长度为n
        vertexList = new ArrayList<String>(n);
        // 邻接矩阵为n*n
        edges = new int[n][n];
        numOfEdges = 0;
      }
    
      //常用的方法
      //返回结点的个数
      public int getNumOfVertex() {
        return vertexList.size();
      }
    
      //显示图对应的矩阵
      public void showGraph() {
        for(int[] vertex:edges){
          System.out.println(Arrays.toString(vertex));
        }
      }
    
      //得到边的数目
      public int getNumOfEdges() {
        return numOfEdges;
      }
    
      //返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
      public String getValueByIndex(int i) {
        return vertexList.get(i);
      }
    
      //返回v1和v2的权值
      public int getWeight(int v1, int v2) {
        return edges[v1][v2];
      }
    
      //插入结点
      public void insertVertex(String vertex) {
        vertexList.add(vertex);
      }
    
      /**
       * 添加边
       *
       * @param v1 表示点的下标即使第几个顶点  "A"-"B" "A"->0 "B"->1
       * @param v2 第二个顶点对应的下标
       * @param weight 表示权,1或者0
       */
      public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
      }
    
    }
    

    输出:

    [0, 1, 1, 0, 0]
    [1, 0, 1, 1, 1]
    [1, 1, 0, 0, 0]
    [0, 1, 0, 0, 0]
    [0, 1, 0, 0, 0]

    图的遍历

    即结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略:

    • 深度优先遍历
    • 广度优先遍历

    前者选择一个领域精通后,再回来进行研究下一个领域;

    后者像创业,从已知出发, 从已经知道的东西逐渐再挖掘感兴趣的部分;

    深度优先遍历(DFS)

    基本思想

    图的深度优先搜索(Depth First Search)

    从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。

    我们可以看到,这样的访问策略是优先往纵向挖掘深入,深度优先搜索是一个递归的过程

    如何实现上图中的DFS步骤呢?

    初始结点为A,从A出发,先标记A已访问,

    A的第一个邻接结点为B,B存在而且未被访问,现在把B当做初始结点

    从B出发,先标记B已访问,B的第一个邻接结点为C,C存在而且未被访问,现在把C当做初始结点

    C 结点的下一个结点D不存在,此时回到B,B的下一个邻接结点尾E

    如何实现上图中的DFS步骤呢?

    初始结点为A,从A出发,先标记A已访问,

    A的第一个邻接结点为B,B存在而且未被访问,现在把B当做初始结点

    从B出发,先标记B已访问,B的第一个邻接结点为C,C存在而且未被访问,现在把C当做初始结点

    C 结点的下一个结点D不存在,此时回到B,B的下一个邻接结点尾E

    思路分析:详解A和B的恩怨纠缠。

    以A为初识结点,A出发,标记A已经访问,如何标记它被访问过呢?即定义一个boolean的数组,把A的下标的元素置为true;

    找到A的下一个邻接结点B,如何找呢?首先我们需要有A的下标,然后找到与A相连的边的长度大于0的结点,也就是说,需要首先遍历A这一行的数组(因为该矩阵中存放的是边的信息),遍历的长度是多少呢?明显是顶点集合的长度,只要找到数组中元素大于1的位置,直接返回其坐标。否则就是没有与A相连的顶点,就返回-1;

    找到的邻接结点B的下标,只要它不为-1,即就是A有相连的邻接结点,我们又不能保证它是不是被访问过,所以首先需要判断B是否被访问过,拿到B的下标去boolean数组中判断,为false,我们就让以B为初识结点进行dfs;如果B被访问过,那好办,我们找到A的邻接结点B的下一个邻接结点C。

    这里如何找到A的邻接结点B的下一个邻接结点C?首先我们把A的位置,B的位置传进去,A用来控制找的是A的邻接结点,B用来控制找到的是B的下一个节点,也就是从B的位置+1处开始进行遍历;

    算法步骤

    1. 访问初始结点v,并标记结点v为已访问。
    2. 查找结点v的第一个邻接结点w。
    3. 若w存在,则继续执行4,如果w不存在,则回到第1步,将从v的下一个结点继续。
    4. 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)。
    5. 查找结点v的w邻接结点的下一个邻接结点,转到步骤3。

    步骤详解:

    1. 初识A的位置为0,标记A,输出0对应的顶点集合中的A,将A的位置0加到队列中;
    2. 只要队列不为空,就取出队列的头部,即取出0,找到0对应的下一个结点位置,即就是B的位置为1,
    3. 这里需要判断B的位置是否存在,如果不为-1就存在,输出1对应的B元素,标记B,将B加入队尾;
    4. 如果B已经被访问了,我们就需要去A的下一个结点B的下一个结点C处去找邻结点。需要以A的位置作为行号,B的位置+1作为遍历起始位置,遍历的长度需要小于顶点集合的长度。

    代码实现

      /**
       * 得到第一个邻接结点的下标 w
       *
       * @return 如果存在就返回对应的下标,否则返回-1
       */
      public int getFirstNeighbor(int index) {
        for (int j = 0; j < vertexList.size(); j++) {
          if (edges[index][j] > 0) {
            return j;
          }
        }
        return -1;
      }
    
      /**
       * @Description: 根据前一个邻接结点的下标来获取下一个邻接结点
       * @Param: [v1, v2]
       * @return: int
       * @Author: benjamin
       * @Date: 2019/8/26
       */
      public int getNextNeighbor(int v1, int v2) {
        for (int j = v2 + 1; j < vertexList.size(); j++) {
          if (edges[v1][j] > 0) {
            return j;
          }
        }
        return -1;
      }
    
      //深度优先遍历算法
      //i 第一次就是 0
      private void dfs(boolean[] isVisited, int i) {
        // 首先输出访问的结点
        System.out.print(getValueByIndex(i) + "->");
        // 将结点设置为已经访问
        isVisited[i] = true;
        // 查找结点i的第一个邻接结点w
        int w = getFirstNeighbor(i);
        // 只要w不为-1,说明有
        while (w != -1){
          if(!isVisited[w]){
            dfs(isVisited,w);
          }
          // 如果该结点已经被访问过,则访问i的下下一个邻接结点
          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);
          }
        }
    
      }
    
    
    #### 广度优先遍历(BFS)
    
    基本思想
    
    图的广度优先搜索(Broad First Search) 。
    
    类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
    
    算法步骤
    
    1. 访问初始结点v并标记结点v为已访问。
    2. 结点v入队列
    3. 当队列非空时,继续执行,否则算法结束。
    4. 出队列,取得队头结点u。
    5. 查找结点u的第一个邻接结点w。
    6. 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:
       1. 若结点w尚未被访问,则访问结点w并标记为已访问。 
       2. 结点w入队列 
       3. 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。
    
    代码实现
    
    ```java
    /**
    * 得到第一个邻接结点的下标 w
    *
    * @return 如果存在就返回对应的下标,否则返回-1
    */
    public int getFirstNeighbor(int index) {
    for (int j = 0; j < vertexList.size(); j++) {
      if (edges[index][j] > 0) {
        return j;
      }
    }
    return -1;
    }
    
    /**
    * @Description: 根据前一个邻接结点的下标来获取下一个邻接结点
    * @Param: [v1, v2]
    * @return: int
    * @Author: benjamin
    * @Date: 2019/8/26
    */
    public int getNextNeighbor(int v1, int v2) {
    for (int j = v2 + 1; j < vertexList.size(); j++) {
      if (edges[v1][j] > 0) {
        return j;
      }
    }
    return -1;
    }
    //对一个结点进行广度优先遍历的方法
    private void bfs(boolean[] isVisited, int i) {
    int u ; // 表示队列的头结点对应下标
    int w ; // 邻接结点w
    //队列,记录结点访问的顺序
    LinkedList queue = new LinkedList();
    //访问结点,输出结点信息
    System.out.print(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.print(getValueByIndex(w) + "=>");
      //标记已经访问
      isVisited[w] = true;
      //入队
      queue.addLast(w);
    }
    //以u为前驱点,找w后面的下一个邻结点
    w = getNextNeighbor(u, w); //体现出我们的广度优先
    }
    }
    }
    
    //遍历所有的结点,都进行广度优先搜索
    public void bfs() {
    isVisited = new boolean[vertexList.size()];// 清空标志位
    for (int j = 0; j < vertexList.size(); j++) {
    if (!isVisited[j]) {
    bfs(isVisited, j);
    }
    }
    }
    
  • 相关阅读:
    Java实现 蓝桥杯VIP 算法训练 字符串逆序
    Java实现 蓝桥杯VIP 算法训练 字符串逆序
    Java实现 蓝桥杯VIP 算法训练 最长字符串
    Java实现 蓝桥杯VIP 算法训练 最长字符串
    Java实现 蓝桥杯VIP 算法训练 最长字符串
    Java实现 蓝桥杯VIP 算法训练 最长字符串
    Java实现 蓝桥杯VIP 算法训练 最长字符串
    Java实现 蓝桥杯VIP 算法训练 成绩的等级输出
    Java实现 蓝桥杯VIP 算法训练 成绩的等级输出
    Qt 自定义model实现文件系统的文件名排序
  • 原文地址:https://www.cnblogs.com/benjieqiang/p/11414409.html
Copyright © 2011-2022 走看看