文章目录

  • 11.1 图的概述
  • 11.1.1 什么是图?
  • 11.1.2 线性表, 树, 图的对比
  • 11.1.3 图的常见概念
  • 11.2 图的存储结构
  • 11.2.1 邻接矩阵(Adjacency Matrix)
  • 11.2.2 邻接表(Adjacency List)
  • 11.3 图的遍历
  • 11.3.1 图的深度优先遍历(DFS)
  • 11.3.2 图的广度优先遍历(BFS)


11.1 图的概述

11.1.1 什么是图?

图(graph)是一种数据结构,它是由顶点的有穷非空集合V和顶点之间边的集合E组成
通常表示为:G(V,E)。其中 G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。

Java的深渊图_数据结构

从图的定义中我们可以注意到它的一个特点:图中数据元素被称为顶点(Vertex)。而在线性表中的数据元素叫元素,树形结构中的数据元素叫做节点。

11.1.2 线性表, 树, 图的对比

11.1.3 图的常见概念

概念

含义

顶点

构成图的基本数据元素


用来连接图中的顶点

无向图,有向图

图中任意两个顶点之间都是没有方向(有方向)的边的图

简单图

不存在资环(顶点到自身的边)和重边(完全相同的边)的图

无向完全图

任意两个顶点之间都有边的无向图

有向完全图

有向图中,任意两个顶点之间都存在方向相反的两条弧

权(Weight)

表示从图中一个顶点到另一个顶点的距离或耗费


与特定顶点相连接的边数


带有权重的图

出度, 入度

有向图中, 出度表示以此顶点为起点的边的数目, 入度表示以此顶点为终点的边的数目

连通图

任意两个顶点之间都相互连通的图

极大联通子图

包含尽可能多的(能联通)的顶点, 即找不到另外一个顶点,使得此顶点能够连接到此极大联通子图的任一顶点

连通分量

极大联通子图的数量

强连通图

有向图,任意两个顶点a,b, 使得a既能够连接到b, b也可以连接到a的图

生成树

n个顶点,n-1条边, 并且保证n个顶点相互连通(不存在环)的图(构成了一个无环的联通子图)

最小生成树

此生成树的权重之和是全部生成树中最小的

Java的深渊图_java_02

11.2 图的存储结构

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

11.2.1 邻接矩阵(Adjacency Matrix)

邻接矩阵是用两个数组来表示图, 一个一维数组存储图中顶点信息, 一个二维数组存储图中边或弧的信息;

  • 在无向图的多维数组中, 1代表两个顶点之间存在边, 相反, 0代表不存在,主对角线全为0表示不存在自环. 比如下图(0,1)为1, 说明顶点0和1之间存在边
  • 另外, 由于无向图的边不分方向, 所以无向图邻接矩阵存储时的二维数组是一个对称矩阵.
  • 在有向图的多维数组中, 数字表示两个顶点之间的权值, 无穷符号表示两个顶点之间不存在边.

邻接矩阵的行和列都是代表着图的第几个顶点。

Java的深渊图_图_03

如上所示图左是一个无向图,右边是该无向图的邻接矩阵:顶点 0 和顶点 1 之间存在一条边,因此 arr[0][1]arr[1][0] 都为 1(仅适用于无向图);顶点 0 和顶点 5 之间没有直接相连的边,因此 arr[0][5]arr[5][0] 都是 0。

[案例演示]



Java的深渊图_Java的深渊图_04

package DataStrcture.graphdemo;

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

public class AdjacencyMatrix {
    /**
     * 1. 存储顶点的vertexs 集合
     * 2. 存储顶点之间边信息的 edges数组
     * 3. 标记是否访问过顶点对应索引的boolean数组 isVisited
     * 4. 顺序存储刚刚遍历过的顶点的索引的队列 queue(linkedlist集合实现)
     */
    ArrayList<String> vertexs; //存储图中的顶点;
    int[][] edges; //存储顶点之间的边信息

    boolean[] isVisited; //标记对应顶点的索引是否被访问
    LinkedList<Integer> queue; //存放刚被访问过的邻接结点

    int numOfEdges;

    //测试方法
    public static void main(String[] args) {
        String[] str = {"A", "B", "C", "D", "E", "F", "G", "H"};
        DataStrcture.graphdemo.BFSDemo bfs = new DataStrcture.graphdemo.BFSDemo(str.length);
        for (String x : str) {
            bfs.addVertex(x);
        }
        bfs.addEdges(0, 2, 1);
        bfs.addEdges(0, 3, 1);
        bfs.addEdges(2, 3, 1);
        bfs.addEdges(2, 1, 1);
        bfs.addEdges(5, 6, 1);
        bfs.addEdges(5, 0, 1);
        bfs.addEdges(6, 4, 1);

        bfs.showGraph();
    }

    //构造器
    public AdjacencyMatrix(int n) {
        vertexs = new ArrayList<String>(n);
        edges = new int[n][n];
        isVisited = new boolean[n];
        queue = new LinkedList<Integer>();
    }

    //添加结点到集合中
    public void addVertex(String str) {
        vertexs.add(str);
    }
    //添加边

    /**
     * @param v1Index 顶点1的索引
     * @param v2Index 顶点2的索引
     * @param weight  顶点1,2之间边的关系, =1有边相连接, =0 无边连接;
     */
    public void addEdges(int v1Index, int v2Index, int weight) {
        edges[v1Index][v2Index] = weight;
        edges[v2Index][v1Index] = weight;
        numOfEdges += 1;
    }

    //以矩阵的形式反映出图
    public void showGraph() {
        System.out.println(" " + vertexs);

        for (int i = 0; i < edges[0].length; i++) {
            System.out.print((char) ('A' + i) + " ");
            for (int j = 0; j < edges.length; j++) {
                System.out.print(edges[i][j] + ", ");
            }
            System.out.println();
        }
        System.out.println();
    }
}
  • 运行结果

Java的深渊图_dfs_05

11.2.2 邻接表(Adjacency List)

图的邻接表(Adjacency List)存储方式是用数组和链表来表示图,其中一个一维数组用来存储顶点,顶点数组中的每个数据元素还需要存储指向该顶点第一个邻接点的指针,每个顶点的所有邻接点构成一个线性表。

邻接矩阵需要为每个顶点都分配n个边的空间, 其实有很多边都是不存在的, 会造成空间有一定的损失, 邻接表的实现只关心存在的边, 不关心不存在的边, 因此没有空间浪费;

如下图就是一个无向图的邻接结构;

Java的深渊图_数据结构_06

如图中右边部分,是无向图的邻接表: 标号为0的代表顶点0, 其后的1,2,3,4 链表元素代表这个0顶点的邻接点为1,2,3,4; 标号为4的顶点的邻接点为0,1,2,5;

11.3 图的遍历

  • 所谓图的遍历,即从某个顶点出发, 遍历图中其余顶点, 且使每个顶点仅被访问一次;
  • 一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略: (1)深度优先遍历 (2)广度优先遍历

图(graph)

图和树的最大区别在于图的下一个节点可能指向已访问过的节点。因此在使用BFS及DFS遍历时,应维护一个集合或者数组, 其中存放已被访问过的节点,在遍历时先判断节点未被访问过再遍历即可。

11.3.1 图的深度优先遍历(DFS)

深度优先遍历(Depth First Search),也称深度优先搜索,简称为 DFS。
遍历规则: 不断沿着顶点邻接结点的邻接结点的深度方向遍历

[DFS的思想和适合解决的问题]

  • DFS的思想:从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底。
  • DFS适合此类题目:给定初始状态跟目标状态,要求判断从初始状态到目标状态是否有解。

[遍历步骤]

  1. 访问指定的起始顶点
  2. 若当前的邻接顶点有未被访问的,则选择第一个邻接的结点访问之, 并在下一步把这个邻接结点作为起始顶点继续向下寻找它的邻接结点;
  3. 反之, 退回到最近访问过的顶点; 直到与起始顶点相通的全部顶点访问完毕;
  4. 若此时图中尚有顶点未被访问, 则再选其中一个顶点作为起始顶点并访问之, 转2; 反之, 遍历结束;
  • 图示如下:

Java的深渊图_dfs_07

[实现思路]- 待完善

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

[核心代码]

/**
* vIndex 深度遍历的起始顶点的索引
*/
public void dfs(int vIndex){
    //输出顶点, 并在标记数组中把这个顶点设为已被访问过(true)
    isVisited[vIndex] = true;
    System.output.println(vertexs.get(vIndex)+"->");

    //往图下面不断的递归, 找出所有"未被访问过的", "与起始顶点相邻或其邻接顶点相邻"
    for(int i=0; i<vertexs.size(); i++){
        if( !isVisited[i] && edges[vIndex][i] ==1){
            dfs(i);
        }
    }
}

//为了避免遗漏度为0的顶点(与其他顶点无边相连) 
//我们需要对顶点集合中所有的顶点都进行一次深度搜索
//当然了, 一定要只访问一次顶点, 故需要加上对标记数组的判断
public void dfsTraverse(){
    if(int i=0; i<vertexs.size(); i++){
        if(! isVisited[i])
        dfs(i);
    }
}

完整代码示例-Java实现图的深度优先遍历(DFS)

11.3.2 图的广度优先遍历(BFS)

广度优先遍历(Breadth First Search),又称为广度优先搜索,简称 BFS。
遍历规则: 以起始顶点v为起点, 由近至远, 依次访问和v路径相同切路径长度为1,2,3…的顶点;

[遍历规则]

  • 广度优先搜索类似于树的层次遍历。
  • 从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点
  • 然后再从这些邻接点出发,同样依次访问它们的邻接点。
  • 按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。
  • 最后还需要做的操作就是查看图中是否存在尚未被访问的顶点,若有,则以该顶点为起始点,重复上述遍历的过程。
  • 图示如下:

Java的深渊图_dfs_08

[实现思路]- 待完善
类似于一个分层搜索的过程, 广度优先遍历需要使用一个队列以保持访问过的结点的顺序, 以便按这个顺序来访问这些结点的邻接结点;



[核心代码]

//广度优先遍历BFS
    public void bfs(int vIndex) {
        //设置起始顶点为访问过, 并把它输出
        //这两条语句也是下面递归的出口哦
        isVisited[vIndex] = true;
        System.out.print(vertexs.get(vIndex) + "->");

        //输出起始顶点的所有的邻接结点,设置访问标志为true, 并把这些顶点的索引存放到队列中
        for (int i = 0; i < vertexs.size(); i++) {
            if (edges[vIndex][i] == 1 && !isVisited[i]) {
                String targetVertex = vertexs.get(i);
                isVisited[i] = true;
                queue.addLast(i);
            }
        }
        //上面的工作(起始顶点的邻接顶点)完成后, 从队首中取出这些邻接顶点,挨个进行递归的BFS
        while (queue.size() > 0) {
            bfs(queue.remove());

        }
    }
    //为了避免遗漏度为0的顶点(与其他顶点无边相连) 
    //我们需要对顶点集合中所有的顶点都进行一次深度搜索
    //当然了, 一定要只访问一次顶点, 故需要加上对标记数组的判断
    public void bfsTraverse() {
        for (int i = 0; i < vertexs.size(); i++) {
            if (!isVisited[i]) {
                bfs(i);
            }
        }
    }
}

完整代码示例-Java实现图的广度优先遍历(BFS)