数据结构与算法:图

前言

本文主要讲解图、图的深度优先遍历、图的广度优先遍历


数据结构与算法文章列表

数据结构与算法文章列表: 点击此处跳转查看


目录

在这里插入图片描述


(一)图基本介绍

(1)为什么要有图(为什么要使用图这种数据结构)

  1. 在线性结构中,数据元素之间满足唯一的线性关系,每个数据元素(除第一个和最后一个外)只有一个直接前趋和一个直接后继

  2. 在树形结构中,数据元素之间有着明显的层次关系,并且每个数据元素只与上一层中的一个元素(parent node)及下一层的多个元素(孩子节点)相关

  3. 如果我们需要多对多的关系时,我们就需要使用图这种数据结构


(2)图的举例说明

在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边连接。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接
在这里插入图片描述
在这里插入图片描述


(3)图的常用概念

  1. 顶点(vertex)
  2. 边(edge)
  3. 路径
  4. 无向图(右图
    在这里插入图片描述
  5. 有向图
  6. 带权图
    在这里插入图片描述

(二)图的表示方式

图的表示方式有两种:邻接矩阵邻接表

(1)邻接矩阵

原理就是用两个数组,一个数组保存顶点集,一个数组保存边集,如下图,如果两个点相通,数组的值则为1;如果不相通,数组的值则为0
在这里插入图片描述


(2)邻接表

邻接表是图的一种链式存储结构。这种存储结构类似于树的孩子链表。对于下图中每个顶点Vi(0),把所有邻接于Vi(0)的顶点Vj(1,2,3,4)链成一个单链表,这个单链表称为顶点Vi的邻接表。
在这里插入图片描述


(三)图的快速入门案例

  1. 要求: 代码实现如下图结构
    在这里插入图片描述
  2. 思路分析
    (1) 存储顶点 String 使用 ArrayList
    (2) 保存矩阵 int[][] edges
  3. 代码实现
package com.lzacking.graph;
import java.util.ArrayList;
import java.util.Arrays;
public class Graph {
    
    

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

     public static void main(String[] args) {
    
    
          String Vertexs[] = {
    
     "A", "B", "C", "D", "E" };
          // A:0
          // B:1
          // C:2
          // D:3
          // E:4
          // 创建图对象
          Graph graph = new Graph(Vertexs.length);

          // 循环的添加顶点
          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); // B-C
          graph.insertEdge(1, 3, 1); //
          graph.insertEdge(1, 4, 1); //

          // 显示一把邻结矩阵
          graph.showGraph();
     }

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

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

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

     // 得到边的数目
     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
      *
      *            edges[1][0] = 1 第1行,第0列 edges[0][1]  = 1 第0行,第1列
      * @param v2
      *            第二个顶点对应的下标
      * @param weight
      *            表示
      */
     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]

(四)图的深度优先遍历介绍

(1)图遍历介绍

所谓图的遍历,就是确定从一个指定的顶点到达其他哪些顶点。图的遍历分为以下两种
(1)深度优先遍历
(2)广度优先遍历


(2)深度优先遍历基本思想

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

  1. 假设初始状态是图中所有顶点都未曾访问过,则可从图中任意一顶点v为初始出发点,首先访问出发点v,并将其标记为已访问过
  2. 然后依次从v出发搜索v的每个邻接点w,若w未曾访问过,则以w作为新的出发点出发,继续进行深度优先遍历,直到图中所有和v有路径相通的顶点都被访问到
  3. 若此时图中仍有顶点未被访问,则另选一个未曾访问的顶点作为起点,重复上述步骤,直到图中所有顶点都被访问到为止

简单的来说,访问一个没有访问过的顶点,将它标记为已访问,再递归地去访问在起始点的邻接表中其他没有访问过的顶点
在这里插入图片描述


(3)深度优先遍历算法步骤

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

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

package com.lzacking.graph;

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

public class Graph {
    
    

    private ArrayList<String> vertexList; // 存储顶点集合
    private int[][] edges; // 存储图对应的邻结矩阵
    private int numOfEdges; // 表示边的数目
    // 定义给数组boolean[], 记录某个结点是否被访问
    private boolean[] isVisited;

    public static void main(String[] args) {
    
    
        String Vertexs[] = {
    
     "A", "B", "C", "D", "E" };

        // 创建图对象
        Graph graph = new Graph(Vertexs.length);
        // 循环的添加顶点
        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.showGraph();

        // 测试一把,我们的dfs遍历是否ok
        System.out.println("深度遍历");
        graph.dfs(); // A->B->C->D->E
    }

    // 构造器
    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 j = 0; j < vertexList.size(); j++) {
    
    
            if (edges[index][j] > 0) {
    
    
                return j;
            }
        }
        return -1;
    }


    // 根据前一个邻接结点的下标来获取下一个邻接结点
    // 比如:结点C找不到下一个邻接结点,回溯到上一个邻接结点B,让B来获取另一个邻接结点D
    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);
        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);
            }
        }
    }


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


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


    // 得到边的数目
    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
     *            表示
     */
    public void insertEdge(int v1, int v2, int weight) {
    
    
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }
}

(五)图的广度优先遍历

(1)广度优先遍历基本思想

  1. 首先访问出发点v
  2. 接着依次访问v的所有未被访问过的邻接点v1,v2,v3,…,vt并均标记为已访问过
  3. 然后再按照v1,v2,… ,vt的次序,访问每一个顶点的所有未曾访问过的顶点并均标记为已访问过,依此类推,直到图中所有和初始出发点v有路径相通的顶点都被访问过为止

(2)广度优先遍历算法步骤

  1. 访问初始结点 v 并标记结点 v 为已访问
  2. 结点 v 入队列
  3. 当队列非空时,继续执行,否则算法结束
  4. 出队列,取得队头结点 u
  5. 查找结点 u 的第一个邻接结点 w
  6. 若结点 u 的邻接结点 w 不存在,则转到步骤 3;否则循环执行以下三个步骤:
    6.1 若结点 w 尚未被访问,则访问结点 w 并标记为已访问
    6.2 结点 w 入队列
    6.3 查找结点 u 的继 w 邻接结点后的下一个邻接结点 w,转到步骤 6

(3)广度优先算法的代码实现

package com.lzacking.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
public class Graph {
    
    

     private ArrayList<String> vertexList; // 存储顶点集合
     private int[][] edges; // 存储图对应的邻结矩阵
     private int numOfEdges; // 表示边的数目
     // 定义给数组boolean[], 记录某个结点是否被访问
     private boolean[] isVisited;

     public static void main(String[] args) {
    
    
          String Vertexs[] = {
    
     "A", "B", "C", "D", "E" };
          // 创建图对象
          Graph graph = new Graph(Vertexs.length);

          // 循环的添加顶点
          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.showGraph();

          System.out.println("广度优先!");
          graph.bfs(); // A->B->C->D-E
     }

     // 构造器
     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 j = 0; j < vertexList.size(); j++) {
    
    
              if (edges[index][j] > 0) {
    
    
                   return j;
              }
          }
          return -1;
     }

     // 根据前一个邻接结点的下标来获取下一个邻接结点
     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 i = 0; i < getNumOfVertex(); i++) {
    
    
              if (!isVisited[i]) {
    
    
                   bfs(isVisited, i);
              }
          }
     }

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

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

     // 得到边的数目
     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
      *            表示
      */
     public void insertEdge(int v1, int v2, int weight) {
    
    
          edges[v1][v2] = weight;
          edges[v2][v1] = weight;
          numOfEdges++;
     }
}

(4)图的深度优先 VS 广度优先

在这里插入图片描述

package com.lzacking.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;

public class Graph {
    
    

     private ArrayList<String> vertexList; // 存储顶点集合
     private int[][] edges; // 存储图对应的邻结矩阵
     private int numOfEdges; // 表示边的数目
     // 定义给数组boolean[], 记录某个结点是否被访问
     private boolean[] isVisited;

     public static void main(String[] args) {
    
    
          // 测试一把图是否创建ok
          int n = 8; // 结点的个数
          String Vertexs[] = {
    
     "1", "2", "3", "4", "5",  "6", "7", "8" };
          // 创建图对象
          Graph graph = new Graph(n);
          // 循环的添加顶点
          for (String vertex : Vertexs) {
    
    
              graph.insertVertex(vertex);
          }
          // 更新边的关系
          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(); // [1->2->4->8->5->3->6->7]
          
          System.out.println();
          
          System.out.println("广度优先!");
          graph.bfs(); // [1->2->3->4->5->6->7->8]
     }

     // 构造器
     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 j = 0; j < vertexList.size(); j++) {
    
    
              if (edges[index][j] > 0) {
    
    
                   return j;
              }
          }
          return -1;
     }

     // 根据前一个邻接结点的下标来获取下一个邻接结点
     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);
          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);
              }
          }
     }

     // 对一个结点进行广度优先遍历的方法
     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 i = 0; i < getNumOfVertex(); i++) {
    
    
              if (!isVisited[i]) {
    
    
                   bfs(isVisited, i);
              }
          }
     }

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

     // 显示图对应的矩阵
     public void showGraph() {
    
    
          for (int[] link : edges) {
    
    
              System.err.println(Arrays.toString(link));
          }
     }
     // 得到边的数目
     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
      *            表示
      */
     public void insertEdge(int v1, int v2, int weight) {
    
    
          edges[v1][v2] = weight;
          edges[v2][v1] = weight;
          numOfEdges++;
     }
}

结果:

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

深度遍历
1->2->4->8->5->3->6->7->
广度优先!
1=>2=>3=>4=>5=>6=>7=>8=>

猜你喜欢

转载自blog.csdn.net/a13027629517/article/details/115280083