图的基本知识

    • 一、图的定义和基本术语
    • 二、图的存储结构
      • (1)数组(邻接矩阵表示法)
      • (2)数组(邻接矩阵)的实现
      • (3)邻接表(链式表示法)
      • (4)邻接表(链式表示法)实现
    • 三、图的遍历
      • (1)深度优先遍历算法
      • (2)广度优先遍历算法
    • 四、图的应用
      • 1、构造最小生成树
        • MST性质
        • 普利姆算法(Prim)
        • 克鲁斯卡尔算法(Kruskal)
      • 2、最短路径
        • 迪杰斯特拉(Dijkstra)
        • 弗洛伊德(Floyd)

一、图的定义和基本术语

图的定义:G=(V,E)

V:顶点(数据元素)的有穷非空集合

E:边的有穷集合

图是包含顶点和边的集合

类似于下图,G1由 V1、V2、V3、V4 四个顶点,四条边组成

G2 由五个顶点,七条边组成。

其中G1中的边带有方向称为有向图, 不带方向的称为无向图

图的基本知识_第1张图片

完全图:任意俩个点都有一条边相连

图的基本知识_第2张图片

稀疏图: 有很少的边或者弧(有向图的边)比较少的图(n< nlogn)

稠密图: 有较多的边或者弧

: 边/弧 带权的图

邻接: 边/弧相连的俩个顶点之间的关系

<> 表示有向,vi -> vj

图的基本知识_第3张图片

顶点的度: 与该顶点相关联的边的数目,记为 TD(v)

在有向图中,顶点的度等于该顶点的入度和出度之和。

顶点 v 的入度是以 v 为终点的有向边的条数记作 ID(v)

顶点 v 的出度是以 v 为始点的有向边的条数 记作 OD(v)

图的基本知识_第4张图片

: 当有向图中仅1个顶点的入度为0,其余顶点的入度均为1,此时是何形状?

答:是一颗树,是一颗有向树

图的基本知识_第5张图片

路径: 接续的边构成的顶点序列

路径长度: 路径上边或弧的数目/权值之和。

假设从0到2,路径有: 0、3、2, 0、1、2,0、2… ,路径长度分别为:2、2、1…

图的基本知识_第6张图片

回路(环): 第一个顶点和最后一个顶点相同的路径

简单路径: 除路径起点和终点可以相同外,其余顶点均不相同的路径

简单回路(简单环): 除路径起点和终点相同外,其余顶点均不相同的路径。

图的基本知识_第7张图片

连通图 (强连通图)

在无 (有) 向图G=(V,{E})中,若对任何两个顶点 v、u都存在从v 到 u 的路径,则称G是连通图 (强连通图)

图的基本知识_第8张图片

权与网

图中边或弧所具有的相关数称为权。表明从一个顶点到另一个顶点的距离或耗费。带权的图称为网

子图:设有两个图G= (V,{E})、G1= (V1,{E1}),若V1 ∈ V,E1 ∈ E,则称 G1是G的子图

图的基本知识_第9张图片

极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边,子图不在连通

生成树:包含无向图G所有顶点的极小连通子图

生成 森林:对于非连通图,由各个连通分量的生成树的集合

图的基本知识_第10张图片

抽象数据类型定义

图的基本知识_第11张图片

二、图的存储结构

图的逻辑结构:多对多

图没有顺序存储结构但可以借助**二维数组(邻接矩阵)**来表示元素间的关系。

链式存储结构

普通的链式存储无法实现图,因为不知道图中某个顶点到底有多少个前驱和后继。

因此可以使用多重表的方式实现。

图的基本知识_第12张图片

(1)数组(邻接矩阵表示法)

建立一个顶点表 (记录各个顶点信息) 和一个邻接矩阵 (表示各个顶点之间关系)

设图A=(V,E)有n个顶点,则

image-20230827140122825

图的邻接矩阵是一个二维数组 :

如果 i 和 j 顶点之间有边或者弧就记为1,否则就记为0

image-20230827140157833

举例说明-无向图的邻接矩阵

v1 与 v2、v4 顶点有边,在二维数组中对应为 arcs[v1][v2]=1, arcs[v1][v4]=1, 其余为 0。

v2与v1、v3、v5顶点有边,在二维数组中对应为 arcs[v2][v1]=1, arcs[v2][v3]=1,arcs[v2][v5]=1其余为 0

以此类推…

图的基本知识_第13张图片

分析1: 通过图中我们可以发现,对角线上的值全为0,这是因为顶点与自身之间没有边

分析2: 求第 i 个顶点的度,就是第 i 行值的和

分析3:如果是完全图,也就是说每俩个顶点都有一条边相连,那么除了对角线的值为0,其余都为 1

举例说明-有向图的邻接矩阵

如果某个顶点有 以自身为起点到其他顶点的弧(出度) 那么记为1,否则为0。

例如: 以 v1为起点的有 v2,v3,在二维数组中 arcs[v1][v2]=1、arcs[v1][v3]=1,其余为0,以此类推…

图的基本知识_第14张图片

注: 在有向图的邻接矩阵中

第i行含义:以结点vi为尾的弧(即出度边)

第i列含义: 以结点vi为头的弧(即入度边)

分析1: 有向图的邻接矩阵可能是不对称的。

分析2

顶点的出度(以该顶点为起点) = 第i行元素值之和

顶点的入度(以该顶点为终点)=第 i 列元素值之和

顶点的度 = 第i行元素值之和 + 第 i 列元素值之和

举例说明-网的邻接矩阵

如果某个顶点有 以自身为起点到其他顶点的弧 那么记为对应的权值,否则为∞。Wij 表示某个顶点的权值

image-20230827141914211

图的基本知识_第15张图片

(2)数组(邻接矩阵)的实现

无向网为例。无向网指:没有方向并且带有权值的图

图的基本知识_第16张图片

1、定义存储结构并且进行初始化。初始化时传入一个顶点数组,计算该数组的长度length,邻接矩阵为 length*length的矩阵。并将矩阵全都初始化为最大值

package ChapterSix.graph;

import java.util.Arrays;

/**
 *
 * Author: YZG
 * Date: 2023/8/27 14:24
 * Description: 实现 无向图的邻接矩阵表示法
 */
public class AMGraph {
    Object[] vexs; // 顶点数组
    Object[][] arcs; // 邻接矩阵
    int vexNum, arcNum; // 记录顶点、边的个数
    /**
     * @description 初始化
     * @date 2023/8/27 14:45
     * @param vexs 表示顶点数组
     * @return
     */
    public AMGraph(Object[] vexs) {
        this.vexs = vexs;
        // 顶点个数
        int length = vexs.length;
        this.vexNum = length;
        this.arcs = new Object[length][length];
        // 初始化邻接矩阵的值皆为∞ ,在Java就用integer的最大值表示
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length; j++) {
                arcs[i][j] = Integer.MAX_VALUE;
            }
        }
    }
}

2、根据传入的顶点、权值构建无向网。

    /**
     * @description 创建无向网
     * @date 2023/8/27 14:50
     * @param v1 顶点1
     * @param v2 顶点2
     * @param weight 顶点1和顶点2之间的权值
     * @return void
     */
    public void createUDN(Object v1, Object v2, int weight) {
        // 找到v1、v2的下标
        int i = findIndex(vexs, v1);
        int j = findIndex(vexs, v2);
        // 防止输入错误
        if (i == -1 || j == -1) throw new RuntimeException("您输入顶点有误");
        // 赋值权值,因为是无向图,所以反向的权值也要赋
        arcs[i][j] = weight;
        arcs[j][i] = weight;
        // 边的个数+1
        this.arcNum++;
    }

3、由于传入的是顶点的名称,还需要一个方法用来找到顶点的下标。

/**
 * @description 根据顶点名称找到对应的下标
 * @date 2023/8/27 14:51
 * @param vexs 顶点数组
 * @param v 顶点名称
 * @return int
 */
public int findIndex(Object[] vexs, Object v) {
    for (int i = 0; i < vexs.length; i++) {
        if (vexs[i]==v) return i;
    }
    return -1;
}

测试

    public static void main(String[] args) {
        AMGraph amGraph = new AMGraph(new Object[]{"v1", "v2", "v3","v4"});
        // 增加边
        amGraph.createUDN("v1","v2",1);
        amGraph.createUDN("v1","v3",2);
        amGraph.createUDN("v1","v4",3);
        amGraph.createUDN("v3","v4",4);
        System.out.println(Arrays.deepToString(amGraph.arcs));
        System.out.println("边的个数:" + amGraph.arcNum);
    }

总结

无向图、有向网 都一样。只不过邻接矩阵存储的数据不一样

无向图:没有权值了,因此在arcs初始化时皆为0,在赋值的时候赋为1

         // 无向图-初始化
          arcs[i][j] = 0; 
		// 无向图-赋值
          arcs[i][j] = arcs[j][i] = 1;

有向网:只需要赋一次权值即可,无需设置反向

 arcs[i][j] = weight;

邻接矩阵的优点

  • 直观、简单、好理解
  • 方便检查任意一对顶点间是否存在边
  • 方便找任一顶点的所有“接点”(有边直接相连的顶点)
  • 方便计算任一顶点的“度”(从该点发出的边数为“出度”,指向该点的边数为“入度”)
    • 无向图: 对应行(或列)非0元素的个数
    • 有向图: 对应行非0元素的个数是"出度", 对应列非0元素的个数是"入度"

缺点

  • 不方便增加和删除顶点
  • 浪费空间,例如存储稀疏图(点很多但是边很少)有大量无效元素
  • 浪费时间,统计稀疏图中一共有多少条边

邻接矩阵的方式和边的个数没有关系,只和顶点的个数有关,存储空间:O(n2

(3)邻接表(链式表示法)

邻接表的表示方法仍然需要一个顶点表,但与邻接矩阵的顶点表不同的是,这个顶点表中元素的类型是一个结点

data用来存放顶点的信息,firstarc 用来存储第一个边结点的地址,也就是说与data相连的顶点。

image-20230827153351665

邻接表中仍然使用一个结点来表示俩个顶点的关系

adjvex 用来表示当前顶点的地址,nextarc表示下一个边顶点的地址,因此对于某一个顶点来说有几个相连的顶点就有几个结点

image-20230827153907158

如果存储网结果,就在多加一个链域用于存储权值

image-20230827154457111

案例

对于v1顶点来说,与它相邻的顶点有 v4,v2,在顶点表中对应的下标为 3、1

图的基本知识_第17张图片

特点

  • 邻接表不唯一,对于相连的顶点可以更改顺序
  • 若无向图中有 n 个顶点、e条边,则其邻接表需 n 个头结点和2e表结点。适宜存储稀疏图
  • 无向图中顶点 vi 的度为第i个单链表中的结点数

存储空间为:O(n+2e)

有向图-邻接表演示

在有向图中只保存以该顶点为起点的弧(出边)的顶点

例如:以v1为起点的弧的顶点为 v2、v3,对应下标 1,2

图的基本知识_第18张图片

特点

  • 顶点为Vi 的出度为第 i 个单链表中的结点个数
  • 顶点 Vi 的入度为整个单链表中邻接点域值是 i -1 的结点个数

找出度易,入度难

(4)邻接表(链式表示法)实现

以无向网为例

1、定义 顶点、边顶点和图的存储结构

public class ALGraph {
    // 存储所有顶点的数组
    VNode[] vertices;
    // 顶点数、边数
    int vexNum,arcNum;
}

// 定义顶点结构
class VNode{
    // 顶点信息
    Object data;
    // 指向第一条边顶点的指针
    ArcNode firstarc;

    @Override
    public String toString() {
        return "VNode{" +
                "data=" + data +
                ", firstarc=" + firstarc +
                '}';
    }
}

// 边顶点类型
class ArcNode{
    // 边顶点的索引位置
    int adjvex;
    // 下一个边顶点的地址
    ArcNode nextarc;
    // 顶点信息
    Object info;

    @Override
    public String toString() {
        return "ArcNode{" +
                "adjvex=" + adjvex +
                ", nextarc=" + nextarc +
                ", info=" + info +
                '}';
    }
}

2、初始化,将顶点信息存储在顶点表,并初始化头指针为NULL

public class ALGraph {
    public static void main(String[] args) {
        ALGraph alGraph = new ALGraph(new Object[]{"A","B","C","D"});
        System.out.println(Arrays.toString(alGraph.vertices));
    }
    // 存储所有顶点的数组
    VNode[] vertices;
    // 顶点数、边数
    int vexNum,arcNum;

    // 初始化 vnodes==顶点集合
    public ALGraph(Object[] vnodes) {
        this.vexNum = vnodes.length;
        this.vertices = new VNode[this.vexNum];
        this.arcNum = 0;
        // 将头顶点赋值,指向第一个边为null
        for (int i = 0; i < this.vexNum; i++) {
            VNode vNode = new VNode();
            vNode.data = vnodes[i];
            vNode.firstarc = new ArcNode();
            this.vertices[i] = vNode;
        }
    }
}

// 定义顶点结构
class VNode{
    // 顶点信息
    Object data;
    // 指向第一条边顶点的指针
    ArcNode firstarc;

    @Override
    public String toString() {
        return "VNode{" +
                "data=" + data +
                ", firstarc=" + firstarc +
                '}';
    }
}

// 边顶点类型
class ArcNode{
    // 边顶点的索引位置
    int adjvex;
    // 下一个边顶点的地址
    ArcNode nextarc;
    // 顶点信息
    Object info;

    @Override
    public String toString() {
        return "ArcNode{" +
                "adjvex=" + adjvex +
                ", nextarc=" + nextarc +
                ", info=" + info +
                '}';
    }
}

3、给定顶点和边的权值生成邻接表

    // 生成邻接表 v1 —— v2
    public void createALGraph(Object v1,Object v2,int weight) {
        // 找到俩个顶点的位置
        int i = findIndex(v1);
        int j = findIndex(v2);
        // 生成新的边顶点
        ArcNode arcNode = new ArcNode();
        arcNode.adjvex = j;
        arcNode.nextarc = vertices[i].firstarc;
        arcNode.info = weight;
        vertices[i].firstarc = arcNode;

        // 由于是无向网,反向也得连接
        ArcNode arcNode1 = new ArcNode();
        arcNode1.adjvex = i;
        arcNode1.nextarc = vertices[j].firstarc;
        arcNode1.info = weight;
        vertices[j].firstarc = arcNode1;
    }

    /**
     * @description 根据顶点名称找到对应的下标
     * @date 2023/8/27 14:51
     * @param v 顶点名称
     * @return int
     */
    public int findIndex(Object v) {
        for (int i = 0; i < vertices.length; i++) {
            if (vertices[i].data == v) return i;
        }
        return -1;
    }

总结:

邻接矩阵与邻接表的关系

图的基本知识_第19张图片

联系:

无论是邻接矩阵还是邻接表,第 i 行都代表 第 i 个顶点与其他顶点的关系。

区别

对于任一确定的无向图,邻接矩阵是唯一的 (行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)

邻接矩阵的空间复杂度为O(n2) , 邻接表的空间复杂度为O(n+e)

用途

邻接矩阵多用于稠密图,而邻接表多用于稀疏图

三、图的遍历

从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。

图的基本知识_第20张图片

图的特点

图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点

怎么避免重复访问呢?

可以设置一个辅助数组 visited[n] ,用来表示被访问过的顶点,初始都为false,如果第 i 个顶点被访问,设置 visited[i] = true

图的遍历方法

  • 深度优先搜索 (Depth First Search-DFS )
  • 广度优先搜索 ( Breadth Frist Search-BFS)

(1)深度优先遍历算法

图的基本知识_第21张图片

案例演示

V1 =》V2 =》 V4 =》 V8 =》 V5 ,发现走不通了回退到 V8 ,仍然没有可以访问的顶点,继续回退

回退到 V1 =》V3 =》V6 =》 V7

图的基本知识_第22张图片

深度优先遍历算法实现

以无向网为例,如下图所示,按照深度优先遍历

图的基本知识_第23张图片

假设从 v1 出发,与之邻接的第一个顶点为 v2,在 visited 数组中发现 v2 并没有被访问过,因此访问 v2,并修改 v2 的访问状态

图的基本知识_第24张图片

访问完 v2,从邻接矩阵中看出,与之邻接的顶点为v1,但是 v1 已经被访问过。回退到 v1,访问下一个邻接顶点 v3,并修改访问状态。

图的基本知识_第25张图片
最后访问v4,结束遍历!

代码实现: 完整代码,包括无向网的创建

public class AMGraph {
    public static void main(String[] args) {
        AMGraph amGraph = new AMGraph(new Object[]{"v1", "v2", "v3", "v4"});
        // 增加边
        amGraph.createUDN("v1", "v2", 1);
        amGraph.createUDN("v1", "v3", 2);
        amGraph.createUDN("v1", "v4", 3);
        amGraph.createUDN("v3", "v4", 4);
        System.out.println(Arrays.deepToString(amGraph.arcs));
        // System.out.println("边的个数:" + amGraph.arcNum);

        // 从v1开始深度遍历
        amGraph.DFS(0);

    }

    Object[] vexs; // 顶点数组
    Object[][] arcs; // 邻接矩阵
    int vexNum, arcNum; // 记录顶点、边的个数
    // 辅助数组,记录顶点是否被访问
    boolean[] visited;

    /**
     * @description 初始化
     * @date 2023/8/27 14:45
     * @param vexs 表示顶点数组
     * @return
     */
    public AMGraph(Object[] vexs) {
        this.vexs = vexs;
        // 顶点个数
        int length = vexs.length;
        this.vexNum = length;
        this.arcs = new Object[length][length];
        this.visited = new boolean[length];
        // 初始化访问数组
        Arrays.fill(visited, false);

        // 初始化邻接矩阵的值皆为∞ ,在Java就用integer的最大值表示
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length; j++) {
                arcs[i][j] = Integer.MAX_VALUE;
                // 无向图
                // arcs[i][j] = 0;
            }
        }
    }

    /**
     * @description 创建无向网
     * @date 2023/8/27 14:50
     * @param v1 顶点1
     * @param v2 顶点2
     * @param weight 顶点1和顶点2之间的权值
     * @return void
     */
    public void createUDN(Object v1, Object v2, int weight) {
        // 找到v1、v2的下标
        int i = findIndex(vexs, v1);
        int j = findIndex(vexs, v2);
        // 防止输入错误
        if (i == -1 || j == -1) throw new RuntimeException("您输入顶点有误");
        // 赋值权重,因为是无向图,所以反向的权值也要赋
        arcs[i][j] = weight;
        arcs[j][i] = weight;
        // 无向图
        // arcs[i][j] = arcs[j][i] = 1;
        // 有向网
        // arcs[i][j] = weight;

        // 边的个数+1
        this.arcNum++;
    }

    /**
     * @description 根据顶点名称找到对应的下标
     * @date 2023/8/27 14:51
     * @param vexs 顶点数组
     * @param v 顶点名称
     * @return int
     */
    public int findIndex(Object[] vexs, Object v) {
        for (int i = 0; i < vexs.length; i++) {
            if (vexs[i] == v) return i;
        }
        return -1;
    }

    /**
     * @description 深度优先遍历算法
     * @date 2023/8/29 22:22
     * @param v 访问的顶点下标
     * @return void
     */
    public void DFS(int v) {
        // 访问当前顶点
        System.out.println(vexs[v]);
        // 更改访问记录值
        visited[v] = true;
        // 访问邻接顶点
        for (int i = 0; i < vexs.length; i++) {
            // 该邻接顶点没有 被访问过
            if (((int) arcs[v][i]) != Integer.MAX_VALUE && !visited[i]) {
                // 递归访问
                DFS(i);
            }
        }
    }
}

(2)广度优先遍历算法

从图的某一结点出发,首先依次访问该结点的所有邻接点v1、v2、…vn ,在按这些顶点被访问的先后次序依次访问与他们相邻接的所有未被访问的顶点。

重复此过程,直到所有顶点均被访问为止!

图的基本知识_第26张图片

算法演示

利用邻接表+队列实现广度优先遍历算法

广度优先算法其实和树的层次遍历有些类似,都是一层一层的遍历,因此我们仍然利用 队列 来实现。

以上面那个图为例,求出它的邻接表,如下图所示:

图的基本知识_第27张图片

初始化访问数组:

图的基本知识_第28张图片

初始化队列:

图的基本知识_第29张图片

1、假设我们从 v1 开始,v1 没有被访问,那么将 v1 结点对应的下标入队,同时标记为已访问,标记完,出队进行访问。

图的基本知识_第30张图片

2、访问完 v1,通过邻接表,找到 与 v1 相连接弧的下标为 1,2 ,对应的结点 v2,v3 ,v2、v3没有被访问, 将 v2,v3的下标入队。此时队列的情况:

图的基本知识_第31张图片

3、入队之后,首先判断 v2 是否被访问过,发现没有则进行标记,然后将 v2 出队访问

图的基本知识_第32张图片

4、v2 出队之后,继续寻找与 v2 相邻接的弧,通过邻接表发现有:0,3,4 对应的结点为:v1、v4、v5,发现 v1 被访问了,v4、v5没有被访问,那么将 v4,v5的下标入队。此时队列情况:

图的基本知识_第33张图片

5、入队之后,v3没有被访问,进行标记,然后将v3出队访问

图的基本知识_第34张图片

6、不断执行上面的操作:找到出队结点的邻接弧 —— 若没有访问过则入队 —— 进行标记 —— 出队访问,直到队列为空。

代码实现

顶点结构

// 定义顶点结构
class VNode {
    // 顶点信息
    Object data;
    // 指向第一条边顶点的指针
    ArcNode firstarc;

    @Override
    public String toString() {
        return "VNode{" +
                "data=" + data +
                ", firstarc=" + firstarc +
                '}';
    }
}

边顶点存储结构

// 边/弧 顶点类型
class ArcNode {
    // 边顶点的索引位置
    int adjvex;
    // 下一个边顶点的地址
    ArcNode nextarc;
    // 顶点信息
    Object info;

    @Override
    public String toString() {
        return "ArcNode{" +
                "adjvex=" + adjvex +
                ", nextarc=" + nextarc +
                ", info=" + info +
                '}';
    }
}

图的存储结构

public class ALGraph {

    // 存储所有顶点的数组
    VNode[] vertices;
    // 顶点数、边数
    int vexNum, arcNum;
    // 辅助数组,记录顶点是否被访问
    boolean[] visited;

    // 初始化 vnodes==顶点集合
    public ALGraph(Object[] vnodes) {
        this.vexNum = vnodes.length;
        this.vertices = new VNode[this.vexNum];
        this.arcNum = 0;
        // 将头顶点赋值,指向第一个边为null
        for (int i = 0; i < this.vexNum; i++) {
            VNode vNode = new VNode();
            vNode.data = vnodes[i];
            vNode.firstarc = new ArcNode();
            this.vertices[i] = vNode;
        }
        // 初始化访问数组
        visited = new boolean[this.vexNum];
        Arrays.fill(visited, false);
    }

    // 生成邻接表 v1 —— v2
    public void createALGraph(Object v1, Object v2, int weight) {
        // 找到俩个顶点的位置
        int i = findIndex(v1);
        int j = findIndex(v2);
        // 生成新的边顶点
        ArcNode arcNode = new ArcNode();
        arcNode.adjvex = j;
        arcNode.nextarc = vertices[i].firstarc;
        arcNode.info = weight;
        vertices[i].firstarc = arcNode;

        // 由于是无向网,反向也得连接
        ArcNode arcNode1 = new ArcNode();
        arcNode1.adjvex = i;
        arcNode1.nextarc = vertices[j].firstarc;
        arcNode1.info = weight;
        vertices[j].firstarc = arcNode1;
    }

    /**
     * @description 根据顶点名称找到对应的下标
     * @date 2023/8/27 14:51
     * @param v 顶点名称
     * @return int
     */
    public int findIndex(Object v) {
        for (int i = 0; i < vertices.length; i++) {
            if (vertices[i].data == v) return i;
        }
        return -1;
    }

广度优先遍历算法实现

LinkedList 为双向循环的队列,addLast 将元素插入队尾(入队),poll 获取对头元素并删除(出队)

  /**
     * @description 广度优先遍历
     * @date 2023/8/29 22:52
     * @param
     * @return void
     */
    public void BFS(int v) {
        // 使用LinkedList模拟循环队列
        LinkedList<Integer> queue = new LinkedList<>();
        // 修改当前顶点访问状态
        visited[v] = true;
        // 将当前顶点插入队尾
        queue.addLast(v);

        while (!queue.isEmpty()) {
            // 出队
            Integer w = queue.poll();
            // 找到w顶点的第一条弧
            ArcNode firstarc = vertices[w].firstarc;
            // 访问
            System.out.println(vertices[w].data);
            // 循环找到与w顶点相邻接的弧
            while (firstarc.nextarc != null) {
                // 弧结点的下标
                int adjvex = firstarc.adjvex;
                // 判断是否访问过
                if (!visited[adjvex]) {
                    // 没有访问过,直接入队
                    queue.addLast(adjvex);
                    // 标记
                    visited[adjvex] =  true;
                }
                // 移动下一个弧的结点
                firstarc = firstarc.nextarc;
            }
        }
    }

四、图的应用

1、构造最小生成树

生成树: 所有顶点均由边连接在一起,但不存在回路的图

图的基本知识_第35张图片

特点

  • 生成树的顶点个数与图的顶点个数相同
  • 生成树是图的极小连通子图,去掉一条边则非连通
  • 一个有n个顶点的连通图的生成树有 n-1 条边,反之则不一定
  • 在生成树中再加一条边必然形成回路

无向图的生成树

我们可以利用图的遍历生成最小生成树,将访问结点走过的边加到生成树当中

图的基本知识_第36张图片

最小生成树: 给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树
也叫最小代价生成树

图的基本知识_第37张图片

最小生成树的典型用途

欲在n个城市间建立通信网,则n个城市应铺n-1条线路

但因为每条线路都会有对应的经济成本,而n个城市最多有n(n-1)/2条线路,那么,如何选择n-1条线路,使总费用最少?

此问题我们就可以转化为求最小生成树,n个城市看做n个顶点,线路看做边,经济成本看做权值。

MST性质

构造最小生成树的算法很多,其中多数算法都利用了MST的性质

MST 性质: 设N =(V E) 是一个连通网,U 是顶点集V的一个非空子集。若边(u,v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树

图的基本知识_第38张图片

MST性质解释

在生成树的构造过程中,图中n个顶点分属两个集合:

  • 已落在生成树上的顶点集: U
  • 尚未落在生成树上的顶点集: V-U

接下来则应在所有连通U中顶点和V-U中顶点的边中选取权值最小的边

image-20230831151037365

普利姆算法(Prim)

算法思想

设 N=(V,E) 是连通图,TE是N上最小生成树中边的集合。

初始令 U={u0} ,u0 ∈ V,TE={}

  • 在所有 U 与 V-U 的边中,找到一条权值最小的边 (u0,v0)
  • 将 (u0,v0) 加入到 TE中, 同时 v0 并入 U
  • 重复上面操作,直到 U=V位置,则 T=(V, TE) 为N的最小生成树

算法演示

1、假设从 V1 开始,U = {V1} V-U= {V2,V3,V4,V5,V6} 相连且权值最小的边为 V1-V3,将 V3 加入到 U, 并且将边加入到 TE 中。

U = {V1, V3},TE={(V1,V3)}

图的基本知识_第39张图片

2、U = {V1,V3} V-U= {V2,V4,V5,V6} 相连且权值最小的边为 V3-V6

将 V6 加入到 U 中,相应的边加入到 TE 中。

U = {V1, V3,V6},TE={(V1,V3), ((V3,V6))}

图的基本知识_第40张图片

3、重复以上操作,直到 U = V

图的基本知识_第41张图片

克鲁斯卡尔算法(Kruskal)

设连通网 N=(V,E),令最小生成树初始状态为只有 n个顶点而无边的非连通图T=(V,{})每个顶点自成一个连通分量

图的基本知识_第42张图片

在E中选取代价最小的边(对边按权值大小升序),若该边依附的顶点落在T中不同的连通分量上(即:不能形成环)则将此边加入到 T中;否则,舍去此边,选取下一条代价最小的边

图的基本知识_第43张图片

俩种算法的比较

图的基本知识_第44张图片

2、最短路径

典型用途:交通网络的问题一从甲地到地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?

那么交通网络用有向图来表示,顶点表示地点,俩个地点的连通用弧表示,权值表示俩地之间的距离。

问题抽象: 在有向网中A点(源点)达 B 点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径

最短路径与最小生成树不同,路径上不一定包含 n个顶点,也不-定包含 n-1条边

第一类问题: 俩点间的最短路径——迪杰斯特拉(Dijkstra)算法

图的基本知识_第45张图片

第二类问题: 某源点到其他各个顶点的最短路径——通常使用弗洛伊德—Floyd算法求解

图的基本知识_第46张图片

迪杰斯特拉(Dijkstra)

迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。

算法步骤

  • 初始化: 先找出从源点V0,到各终点V的直达路径 (V0,Vk),即通过一条弧到达的路径
  • 选择: 从这些路径中找出一条长度最短的路径 (V0,U)
  • 更新: 然后对其余各条路径进行适当调整
    • 若在图中存在弧 (U,Vk) ,且 (V0,U) + (U,Vk) < (V0,Vk)则以路径 V0,U,Vk) 代替 (V0,Vk)
    • 依此类推在调整后的各条路径中,再找长度最短的路径

迪杰斯特拉 (Dijkstra)算法: 按路径长度递增次序产生最短路径

1、把V分成俩组

  • S:已求出最短路径的顶点的结合
  • T = V-S : 尚未确定最短路径的顶点集合

2、将 T 中顶点按最短路径递增的次序加入到 S 中

  • 保证从源点到 S 中各顶点的最短路径都不大于 源点到T中任何顶点的最短路径长度。

算法演示

初始 S= {V0} , T = {其余顶点}

T中顶点对应的距离值用辅助数组D存放,若有直达的路径,则存储距离值,若不存在则为∞

图的基本知识_第47张图片

图的基本知识_第48张图片

1、从 V0 开始,找到能够直达的顶点有:V2、V1、V6、V4,其余顶点的距离皆为 ∞

图的基本知识_第49张图片

2、在这些直达路径中,找到最短的路径的顶点,加到 S 中。此时 S = {V0 , V2}

T = {V1 , V3 , V4 , V5, V6} ,

图的基本知识_第50张图片

3、 加入 V2 顶点后,以 V2 顶点作为中间顶点,若V0 距离这些顶点是否变短了,就更新表中的距离。

以 V3 为例 ,未加入 V2 之前是∞ ,加入之后,路径为 13 ,就更新表中的路径为 13…以此类推

在更新后的路径中,继续找最短的路径,并将顶点加入到 S 中

此时 S = {V0 ,V1 , V2} , T = { V3 , V4 , V5, V6} ,

图的基本知识_第51张图片

3、重复上面的操作,直到 S=V,找到所有的顶点即可。

弗洛伊德(Floyd)

求所有顶点间的最短路径:

方法一:每次以一个顶点为源点,重复执行 Dijkstra 算法

方法二: 弗洛伊德算法

算法思想

逐个顶点试探

从vi到vj的所有可能存在的路径中,选出一条长度最短的路径

案例演示

图的基本知识_第52张图片

1、初始时设置一个邻接矩阵表示图,存在弧为 权值,否则为 ∞ ,对角线为0

图的基本知识_第53张图片

2、逐步试着在原直接路径中增加中间顶点,若加入中间顶点后路径变短,则修改之; 否则,维持原值。所有顶点试探完毕,算法结束

(1)加入A顶点, A——B、C都没有变化,B——A、C也没有变化,C —— B,由于A点的加入,变为可达,C-A-B,路径为7,更新表中的权值

图的基本知识_第54张图片

(2)加入B顶点后,A-C路径为 11,加入B顶点,A-B-C 路径为 6,比原来路径小,更新表中的权值

image-20230904114116831

(3)加入C后,B-A 变成了 B-C-A,路径变为5

图的基本知识_第55张图片

你可能感兴趣的:(#,数据结构(青岛大学王卓老师版),算法)