13.图

目录

1.图的基本介绍
2.图的存储方式
 2.1 顺序存储结构
 2.2 邻接表存储结构
3.图的遍历(搜索)算法
 3.1 深度优先搜索(DFS)
 3.2 广度优先搜索(BFS)
 3.3 图的遍历总结

1.图的基本介绍

图的介绍:

数据之间的关系有 3 种,分别是 “一对一”、“一对多” 和 “多对多”,前两种关系的数据可分别用线性表和树结构存储。而存储具有"多对多"逻辑关系数据的结构称为——图存储结构。
在这里插入图片描述
例如上图1所示为存储 V1、V2、V3、V4 的图结构,从图中可以清楚的看出数据之间具有的"多对多"关系。例如,V1 与 V4 和 V2 建立着联系,V4 与 V1 和 V3 建立着联系,以此类推。
 
图存储结构中,习惯上用 Vi 表示图中的顶点,且所有顶点构成的集合通常用 V 表示,如图 1 中顶点的集合为 V={V1,V2,V3,V4}。

图的常用概念:

  1. 顶点:图与链表不同,图中存储的各个数据元素被称为顶点(而不是节点);
  2. 无向图:如上图中各个顶点之间的关系是"双向"的,这种图结构的表现类型称为无向图。
  3. 有向图:与无向图相反,各个顶点之间的关系并不是"双向"的称为有向图。如下图2所示,V4 只与 V1 存在联系(从 V4 可直接找到 V1),而与 V3 没有直接联系;同样,V3 只与 V4 存在联系(从 V3 可直接找到 V4),而与 V1 没有直接联系,以此类推:
    在这里插入图片描述
  4. 弧头和弧尾:有向图中,无箭头一端的顶点通常被称为"初始点"或"弧尾",箭头直线的顶点被称为"终端点"或"弧头";
  5. 入度和出度:对于有向图中的一个顶点 V 来说,箭头指向 V 的弧的数量为 V 的入度(InDegree,记为 ID(V));箭头远离 V 的弧的数量为 V 的出度(OutDegree,记为OD(V))。以图 2 中的顶点 V1为例,该顶点的入度为 1,出度为 2(该顶点的度为 3)。
  6. 边和弧:无向图中描述两顶点V1 和 V2之间的关系可以用 (V1,V2) 来表示,而有向图中描述从 V1 到 V2 的"单向"关系用<V1,V2> 来表示。由于图存储结构中顶点之间的关系是用线来表示的,因此 (V1,V2) 还可以用来表示无向图中连接 V1 和 V2 的线,又称为;同样,<V1,V2> 也可用来表示有向图中从 V1 到 V2 带方向的线,又称为
  7. 集合 VR 的含义:并且,图中习惯用 VR 表示图中所有顶点之间关系的集合。例如,图 1 中无向图的集合 VR={(v1,v2),(v1,v4),(v1,v3),(v3,v4)},图 2 中有向图的集合 VR={<v1,v2>,<v1,v3>,<v3,v4>,<v4,v1>};
  8. 路径和回路:无论是无向图还是有向图,从一个顶点到另一顶点途径的所有顶点组成的序列(包含这两个顶点),称为一条路径。如果路径中第一个顶点和最后一个顶点相同,则此路径称为"回路"(或"环")。
     
    并且,若路径中各顶点都不重复,此路径又被称为"简单路径";同样,若回路中的顶点互不重复,此回路被称为"简单回路"(或简单环)。例如图 1 中,从 V1 存在一条路径还可以回到 V1,此路径为 {V1,V3,V4,V1},这是一个回路(环),而且还是一个简单回路(简单环)。
  9. 权和网的含义:在某些实际场景中,图中的每条边(或弧)会赋予一个实数来表示一定的含义,这种与边(或弧)相匹配的实数被称为"权",而带权的图通常称为网。如下图 3 所示,就是一个网结构:
    在这里插入图片描述
  10. 子图:指的是由图中一部分顶点和边构成的图,称为原图的子图。

图结构的分类:

根据不同的特征,图又可分为完全图,连通图、稀疏图和稠密图:

  • 完全图:若图中各个顶点都与除自身外的其他顶点有关系,这样的无向图称为完全图(如下图 4a))。同时,满足此条件的有向图则称为有向完全图(下图 4b))。
    在这里插入图片描述
    具有 n 个顶点的完全图,图中边的数量为 n(n-1)/2;而对于具有 n 个顶点的有向完全图,图中弧的数量为 n(n-1)。
  • 稀疏图和稠密图:这两种图是相对存在的,即如果图中具有很少的边(或弧),此图就称为"稀疏图";反之,则称此图为"稠密图"。
     
    稀疏和稠密的判断条件是:e<nlogn,其中 e 表示图中边(或弧)的数量,n 表示图中顶点的数量。如果式子成立,则为稀疏图;反之为稠密图。
  • 连通图:图中从一个顶点到达另一顶点,若存在至少一条路径,则称这两个顶点是连通着的。如下图中,虽然 V1 和 V3 没有直接关联,但从 V1 到 V3 存在两条路径,分别是 V1-V2-V3 和 V1-V4-V3,因此称 V1 和 V3 之间是连通的。
    在这里插入图片描述
    无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图。例如下图中的无向图就是一个连通图,因为此图中任意两顶点之间都是连通的。
    在这里插入图片描述
    若无向图不是连通图,但图中某个子图符合连通图的性质,则称该子图为连通分量,该子图也称"极大连通子图"。如下图所示,虽然图 a) 中的无向图不是连通图,但可以将其分解为 3 个"最大子图"(图 b)),它们都满足连通图的性质,因此都是连通分量。在这里插入图片描述
    需要注意的是,连通分量的提出是以"整个无向图不是连通图"为前提的,因为如果无向图是连通图,则其无法分解出多个最大连通子图,因为图中所有的顶点之间都是连通的。
  • 强连通图:有向图中,若任意两个顶点 Vi 和 Vj,满足从 Vi 到 Vj 以及从 Vj 到 Vi 都连通,也就是都含有至少一条通路,则称此有向图为强连通图。如下图所示就是一个强连通图。
    在这里插入图片描述
    与此同时,若有向图本身不是强连通图,但其包含的最大连通子图具有强连通图的性质,则称该子图为强连通分量。如下图所示,整个有向图虽不是强连通图,但其含有两个强连通分量。
    在这里插入图片描述
    总的来说,连通图是在无向图的基础上对图中顶点之间的连通做了更高的要求,而强连通图是在有向图的基础上对图中顶点的连通做了更高的要求。

生成树、生成森林:

对连通图进行遍历,过程中所经过的边和顶点的组合可看做是一棵普通树,通常称为生成树。如图下所示,图 a) 是一张连通图,图 b) 是其对应的 2 种生成树。(连通图中,由于任意两顶点之间可能含有多条通路,遍历连通图的方式有多种,往往一张连通图可能有多种不同的生成树与之对应。)
在这里插入图片描述
连通图中的生成树必须满足以下 2 个条件:

  1. 包含连通图中所有的顶点;
  2. 任意两顶点之间有且仅有一条通路;

因此,连通图的生成树具有这样的特征,即生成树中边的数量 = 顶点数 - 1。
 
生成树是对应连通图来说,而生成森林是对应非连通图来说的。非连通图可分解为多个连通分量,而每个连通分量又各自对应多个生成树(至少是 1 棵),因此与整个非连通图相对应的,是由多棵生成树组成的生成森林。如下图1所示,这是一张非连通图,可分解为 3 个连通分量,其中各个连通分量对应的一种生成树如下图2所示:
在这里插入图片描述
在这里插入图片描述
因此,多个连通分量对应的多棵生成树就构成了整个非连通图的生成森林。

2.图的存储方式

2.1 顺序存储结构

使用图结构表示的数据元素之间虽然具有“多对多”的关系,但是同样可以采用顺序存储,也就是使用数组有效地存储图。

使用数组存储图时,需要使用两个数组,一个数组存放图中顶点本身的数据(一维数组),另外一个数组用于存储各顶点之间的关系(二维数组)。
存储图中各顶点本身数据,使用一维数组就足够了;存储顶点之间的关系时,要记录每个顶点和其它所有顶点之间的关系,所以需要使用二维数组。
不同类型的图,存储的方式略有不同,根据图有无权,可以将图划分为两大类:图和网 。在使用二维数组存储顶点之间的关系时,如果顶点之间存在边或弧,图则用1表示,网则用权值表示,反之则用0表示。

数据结构(JAVA):

package graph;

import java.util.ArrayList;

public class Graph {
    private ArrayList<String> vertexs;//存储图中顶点数据
    private int[][] edges;//二维数组,记录顶点之间的关系:边
    private int edgeNum;//记录图的弧(边)数
    private int kind;记录图的种类

    public Graph(int vertexNum){
        this.vertexs = new ArrayList<>();
        this.edges = new int[vertexNum][vertexNum];
        this.edgeNum = 0;
    }
}

例如下图中的无向图,除了存储图中各顶点本身具有的数据外,还需要使用二维数组存储任意两个顶点之间的关系。由于该图为无向图,各顶点没有权值,所以如果两顶点之间有关联,相应位置记为 1 ;反之记为 0 。
在这里插入图片描述
构建的二维数组如下图所示:在此二维数组中,每一行代表一个顶点,依次从 V1 到 V5 ,每一列也是如此。比如 edges[0][1] = 1 ,表示 V1 和 V2 之间有边存在;而 edges[0][2] = 0,说明 V1 和 V3 之间没有边。
在这里插入图片描述
对于无向图来说,二维数组构建的二阶矩阵,实际上是对称矩阵,在存储时就可以采用压缩存储的方式存储下三角或者上三角。并且,通过二阶矩阵,可以直观地判断出各个顶点的度,为该行(或该列)非 0 值的和。例如,第一行有两个 1,说明 V1 有两个边,所以度为 2。

2.2 邻接表存储结构

通常,图更多的是采用链表存储,具体的存储方法有 3 种,分别是邻接表、邻接多重表和十字链表。邻接表既适用于存储无向图,也适用于存储有向图。

"邻接点"的概念:
在图中,如果两个点相互连通,即通过其中一个顶点,可直接找到另一个顶点,则称它们互为邻接点。邻接指的是图中顶点之间有边或者弧的存在。

邻接表:

邻接表存储图的实现方式是,给图中的各个顶点独自建立一个链表,用节点存储该顶点,用链表中其他节点存储各自的临界点。
与此同时,为了便于管理这些链表,通常会将所有链表的头节点存储到数组中(也可以用链表存储)。也正因为各个链表的头节点存储的是各个顶点,因此各链表在存储临界点数据时,仅需存储该邻接顶点位于数组中的位置下标即可。
例如存储下图 a) 所示的有向图,其对应的邻接表如图 b) 所示:
在这里插入图片描述
以顶点 V1 为例,与其相关的邻接点分别为 V2 和 V3,因此存储 V1 的链表中存储的是 V2 和 V3 在数组中的位置下标 1 和 2。
从上图中可以看出,存储各顶点的节点结构分为两部分,数据域和指针域。数据域用于存储顶点数据信息,指针域用于链接下一个节点,如下图所示:
在这里插入图片描述
在实际应用中,除了上图这种节点结构外,对于用链接表存储网(边或弧存在权)结构,还需要节点存储权的值,因此需使用下图中的节点结构:
在这里插入图片描述

数据结构(JAVA):

package graph;

class ALNode{
    int adjvex;//邻接点在数组中的位置下标
    ALNode next;//指向下一个邻接点的指针
    int info;//信息域(权值)
}

class Vertex{
    String data;//顶点的数据域
    ALNode next;//指向邻接点的指针
}

public class ALGraph {
    private Vertex[] vertexs;//图中顶点的数组
    private int edgeNum, vertexNum;记录图中顶点数和边或弧数
    private int kind;//记录图的种类
}

计算邻接表顶点的出度和入度:

  • 使用邻接表计算无向图中顶点的入度和出度会非常简单,只需从数组中找到该顶点然后统计此链表中节点的数量即可。
  • 而使用邻接表存储有向图时,通常各个顶点的链表中存储的都是以该顶点为弧尾的邻接点,因此通过统计各顶点链表中的节点数量,只能计算出该顶点的出度,而无法计算该顶点的入度。
    对于利用邻接表求某顶点的入度,有两种方式:
  1. 遍历整个邻接表中的节点,统计数据域与该顶点所在数组位置下标相同的节点数量,即为该顶点的入度;
  2. 建立一个逆邻接表,该表中的各顶点链表专门用于存储以此顶点为弧头的所有顶点在数组中的位置下标。比如说,建立一张图 1a) 对应的逆邻接表,如下图所示:
    在这里插入图片描述

对于具有 n 个顶点和 e 条边的无向图,邻接表中需要存储 n 个头结点和 2e 个表结点。在图中边或者弧稀疏的时候,使用邻接表要比邻接矩阵更加节省空间。

3.图的遍历(搜索)算法

所谓图的遍历,即是对顶点的访问。一个图有那么多个顶点,如何遍历这些顶点,需要特定策略。对图中顶点进行遍历,常用的方式有两种:深度优先搜索和广度优先搜索。

3.1 深度优先搜索(DFS)

图的深度优先搜索(Depth First Search)基本思想:

  1. 深度优先遍历,从初始访问顶点出发,初始访问顶点可能有多个邻接点,深度优先遍历的策略就是首先访问第一个邻接点,然后再以这个被访问的邻接点作为初始顶点,访问它的第一个邻接点, 可以这样理解: 每次都在访问完当前顶点后首先访问当前顶点的第一个邻接点。(一条路走到底)
  2. 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个顶点的所有邻接点进行横向访问。
  3. 显然,深度优先搜索是一个递归的过程。

示例演示:

在这里插入图片描述
例如上图是一个无向图,采用深度优先算法遍历这个图的过程为:

  1. 首先任意找一个未被遍历过的顶点,例如从 V1 开始,由于 V1 率先访问过了,所以,需要标记 V1 的状态为访问过;
  2. 然后遍历 V1 的邻接点,访问 V2 ,并做标记,然后访问 V2 的邻接点 V4 (做标记),然后 V8 ,然后 V5 ;
  3. 当继续遍历 V5 的邻接点时,根据之前做的标记显示,所有邻接点都被访问过了。此时,从 V5 回退到 V8 ,看 V8 是否有未被访问过的邻接点,如果没有,继续回退到 V4 , V2 , V1 ;
  4. 通过查看 V1 ,找到另一个未被访问过的顶点 V3 ,继续遍历,然后访问 V3 邻接点 V6 ,然后 V7 ;
  5. 由于 V7 没有未被访问的邻接点,所有回退到 V6 ,继续回退至 V3 ,最后到达 V1 ,发现没有未被访问的;
  6. 最后一步需要判断是否所有顶点都被访问,如果还有没被访问的,以未被访问的顶点为第一个顶点,继续依照上边的方式进行遍历。

根据上边的过程,可以得到图 1 通过深度优先搜索获得的顶点的遍历次序为:V1 -> V2 -> V4 -> V8 -> V5 -> V3 -> V6 -> V7

深度优先搜索的归纳:

所谓深度优先搜索,是从图中的一个顶点出发,每次遍历当前访问顶点的邻接点,一直到访问的顶点没有未被访问过的邻接点为止。然后采用依次回退的方式,查看来的路上每一个顶点是否有其它未被访问的邻接点。访问完成后,判断图中的顶点是否已经全部遍历完成,如果没有,以未访问的顶点为起始点,重复上述过程。深度优先搜索是一个不断回溯的过程。

代码实现(JAVA):

package graph;

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

public class Graph {
    private ArrayList<String> vertexs;//存储图中顶点数据
    private int[][] edges;//二维数组,记录顶点之间的关系:边
    private int edgeNum;//记录图的弧(边)数
    private int kind;记录图的种类:0-无向图,1-有向图
    private boolean[] visited;

    public Graph(int vertexNum, int kind){
        this.vertexs = new ArrayList<>();
        this.edges = new int[vertexNum][vertexNum];
        this.edgeNum = 0;
        this.kind = kind;
        this.visited = new boolean[vertexNum];
    }

    public static void main(String[] args) {
        String vertexs[] = {"V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8"};
        //创建图对象
        Graph graph = new Graph(vertexs.length, 0);
        //循环的添加顶点
        for(String vertex: vertexs) {
            graph.addVertex(vertex);
        }
        //添加边的关系
        graph.addEdge(0, 1, 1);
        graph.addEdge(0, 2, 1);
        graph.addEdge(1, 3, 1);
        graph.addEdge(1, 4, 1);
        graph.addEdge(2, 5, 1);
        graph.addEdge(2, 6, 1);
        graph.addEdge(3, 7, 1);
        graph.addEdge(4, 7, 1);
        graph.addEdge(5, 6, 1);
        //显示顺序存储的矩阵
        graph.showGraph();
        //深度优先搜索
        System.out.println("图的深度优先遍历结果:");
        graph.dfs();
    }

    public void addVertex(String vertex) {
        vertexs.add(vertex);
    }

    public void addEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        if (this.kind==0){
            edges[v2][v1] = weight;
        }
        edgeNum++;
    }

	public String printVertex(int index){
        return vertexs.get(index);
    }

    //显示顺序存储图的矩阵
    public void showGraph() {
        for(int[] link : edges) {
            System.out.println(Arrays.toString(link));
        }
    }

    //查找与数组下标为v的顶点之间有边的顶点,返回它在数组中的下标(得到顶点的邻接点下标)
    public int firstAdjVex(int v){
        for (int i = 0; i < vertexs.size(); i++) {
            if (edges[v][i] > 0){
                return i;
            }
        }
        return -1;
    }

    //根据顶点和顶点的一个邻接点的下标来获取顶点的下一个邻接点
    //例如根据示例图中的V1和V1的一个邻接点V2,获取V1的下一个邻接点V3的下标
    public int nextAdjVex(int v1, int v2){
        for(int i = v2 + 1; i < vertexs.size(); i++) {
            if(edges[v1][i] > 0) {
                return i;
            }
        }
        return -1;
    }

    public void dfs(){
        //将用做标记的visited数组初始化为false
        for (int i = 0; i < vertexs.size(); i++) {
            visited[i] = false;
        }
        //从第一个顶点开始递归深度优先搜索
        // dfs(0);
        //可能存在非连通图的情况,所以需要遍历所有的顶点,都进行dfs。
        // 访问过的顶点不会进行重复dfs
        for(int i = 0; i < vertexs.size(); i++) {
            if(!visited[i]) {
                dfs(i);
            }
        }
    }

    public void dfs(int i){
        if (visited[i]){//当前顶点已经被访问则直接return
            return;
        }
        //输出访问的顶点i
        System.out.print(vertexs.get(i) + " ");
        //对已经访问的顶点标记为true
        visited[i] = true;
        //查找顶点i的第一个邻接结点w
        int w = firstAdjVex(i);
        while(w != -1) {//不等于-1说明有邻接点
            //邻接点存在则递归深度优先搜索
            dfs(w);
            //深度优先搜索到底后,回溯到当前顶点,继续深度优先搜索其下一个邻接点
            w = nextAdjVex(i, w);
        }
    }

}

在这里插入图片描述

3.2 广度优先搜索(BFS)

图的广度优先搜索(Broad First Search)基本思想:

图的广度优先搜索类似于树的层次遍历。从图中的某一顶点出发,遍历每一个顶点时,依次遍历其所有的邻接点,然后再从这些邻接点出发,同样依次访问它们的邻接点。按照此过程,直到图中所有被访问过的顶点的邻接点都被访问到。广度优先搜索的实现需要借助队列这一特殊数据结构。

示例演示:

在这里插入图片描述
仍然以该无向图为例,假设 V1 作为起始点,遍历其所有的邻接点 V2 和 V3 ,以 V2 为起始点,访问邻接点 V4 和 V5 ,以 V3 为起始点,访问邻接点 V6 、 V7 ,以 V4 为起始点访问 V8 ,以 V5 为起始点,由于 V5 所有的起始点已经全部被访问,所有直接略过, V6 和 V7 也是如此。以 V1 为起始点的遍历过程结束后,判断图中是否还有未被访问的点,由于图 1 中没有了,所以整个图遍历结束。遍历顶点的顺序为:V1 -> V2 -> v3 -> V4 -> V5 -> V6 -> V7 -> V8

代码实现(JAVA):

public void bfs(){
        //将用做标记的visited数组初始化为false
        for (int i = 0; i < vertexs.size(); i++) {
            visited[i] = false;
        }
        //可能存在非连通图的情况,所以需要遍历所有的顶点,都进行dfs。
        // 访问过的顶点不会进行重复dfs
        for(int i = 0; i < vertexs.size(); i++) {
            if(!visited[i]) {
                bfs(i);
            }
        }
    }

    public void bfs(int i){
        if (visited[i]){
            return;
        }
        //访问顶点,输出顶点信息
        System.out.print(printVertex(i)+" ");
        //标记为已访问
        visited[i] = true;
        //队列,记录顶点访问的顺序
        Queue<Integer> queue = new LinkedList<>();
        //将顶点加入队列
        queue.offer(i);
        //开始广度优先搜索,遍历所有邻接点
        while( !queue.isEmpty()) {
            //取出队列的头顶点下标
            int v = queue.poll();
            //得到第一个邻接点的下标 w
            int w = firstAdjVex(v);
            while(w != -1) {//有邻接点
                //是否访问过
                if(!visited[w]) {
                    System.out.print(printVertex(w)+" ");
                    //标记已经访问
                    visited[w] = true;
                    //入队
                    queue.offer(w);
                }
                //以u为前驱点,找w后面的下一个邻结点
                w = nextAdjVex(v, w); //体现出我们的广度优先
            }
        }
    }

在这里插入图片描述

3.3 图的遍历总结

上述介绍了两种遍历图的方式:深度优先搜索算法和广度优先搜索算法。深度优先搜索算法的实现运用的主要是回溯法,类似于树的先序遍历算法,可以用递归或者栈来实现。广度优先搜索算法借助队列的先进先出的特点,类似于树的层次遍历。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值