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);
    }
    }
    }
    
  • 相关阅读:
    第01篇 说一下Setting,我一直没有讲过
    簡單委託介紹
    委託和事件
    wcf
    網站和項目的發佈問題
    jquery和js使用技巧
    js中String.prototype.format類似于.net中的string.formitz效果
    [剑指Offer] 6.旋转数组的最小数字(二分法)
    [剑指Offer] 5.用两个栈实现队列
    [剑指Offer] 4.重建二叉树
  • 原文地址:https://www.cnblogs.com/benjieqiang/p/11414409.html
Copyright © 2011-2022 走看看