04树 + 堆 + 优先队列 + 图(D4_图(D1_基础学习))

目录

一、引言

二、基本概念

三、图的定义

四、图的基本概念和术语

[1. 有向图](#1. 有向图)

[2. 无向图](#2. 无向图)

[3. 简单图](#3. 简单图)

[4. 多重图](#4. 多重图)

[5. 完全图(也称简单完全图)](#5. 完全图(也称简单完全图))

[6. 子图](#6. 子图)

[7. 连通、连通图和连通分量](#7. 连通、连通图和连通分量)

[8. 强连通图、强连通分量](#8. 强连通图、强连通分量)

[9. 生成树、生成森林](#9. 生成树、生成森林)

[10. 顶点的度、入度和出度](#10. 顶点的度、入度和出度)

[11. 边的权和网](#11. 边的权和网)

[12. 稠密图、稀疏图](#12. 稠密图、稀疏图)

[13. 路径、路径长度和回路](#13. 路径、路径长度和回路)

[14. 简单路径、简单回路](#14. 简单路径、简单回路)

[15. 距离](#15. 距离)

[16. 有向树](#16. 有向树)

五、图的存储结构

[1. 邻接矩阵](#1. 邻接矩阵)

[2. 邻接表](#2. 邻接表)

[3. 十字链表](#3. 十字链表)

[4. 邻接多重表](#4. 邻接多重表)

[5. 边集数组](#5. 边集数组)

六、图的遍历

[1. 深度优先遍历](#1. 深度优先遍历)

[1.1. DFS算法](#1.1. DFS算法)

[1.2. DFS算法的性能分析](#1.2. DFS算法的性能分析)

[1.3. 深度优先的生成树和生成森林](#1.3. 深度优先的生成树和生成森林)

[2. 广度优先遍历](#2. 广度优先遍历)

[2.1. BFS算法](#2.1. BFS算法)

[2.2. BFS算法性能分析](#2.2. BFS算法性能分析)

[3. 图的遍历与图的连通性](#3. 图的遍历与图的连通性)

[3.1. 普里姆(Prim)算法](#3.1. 普里姆(Prim)算法)

[3.2. 克鲁斯卡尔(Kruskal)算法](#3.2. 克鲁斯卡尔(Kruskal)算法)

[4. 总结](#4. 总结)

七、最短路径

[1. 迪杰斯特拉( Dijkstra )算法](#1. 迪杰斯特拉( Dijkstra )算法)

[2. 弗洛伊德( Floyd )算法](#2. 弗洛伊德( Floyd )算法)

八、拓扑排序

[1. 定义](#1. 定义)

[2. 算法](#2. 算法)

九、关键路径

[1. 定义](#1. 定义)

[2. 算法](#2. 算法)

十、知识小结


一、引言

图(Graph)描述的是一些个体之间的关系。

与线性表和二叉树不同的是:这些个体之间既不是前驱后继的顺序关系,也不是祖先后代的层次关

系,而是错综复杂的网状关系。图也是数据结构中经常使用的一种结构,让我们来学习一下使用图

的算法吧。

【知识框架】

二、基本概念

线性表中, 数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一

个直接后继。

树形结构中, 数据元素之间有着明显的层次关系,并且每一层上的数据元素可能和下一层中多

个元素相关,但只能和上一层中一个元素相关。图是一种较线性表和树更加复杂的数据结构。在图

形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。

三、图的定义

图(Graph)是由顶点的有穷非空集合V ( G ) 和顶点之间边的集合E ( G )组成,通常表示为: G = ( V ,

E ) ,其中,G表示个图,V 是图G中顶点的集合,E是图G中边的集合。若V = { v 1 , v 2 , . . . , v n

} ,则用∣ V ∣表示图G中顶点的个数,也称图G的阶,E = { ( u , v ) ∣ u ∈ V , v ∈ V } ,用∣ E ∣表

示图G中边的条数。

注意:线性表可以是空表,树可以是空树,但图不可以是空图。就是说,图中不能一个顶点也没

有,图的顶点集V一定非空,但边集E可以为空,此时图中只有顶点而没有边。

四、图的基本概念和术语

1. 有向图

若E是有向边(也称弧)的有限集合时,则图G为有向图。弧是顶点的有序对,记为<v, w>,其中v,w

是顶点,v称为弧尾,w称为弧头,<v,w>称为从顶点v到顶点w的弧,也称v邻接到w,或w邻接自

v。

图(a)所示的有向图G1 可表示为

2. 无向图

若E是无向边(简称边)的有限集合时,则图G为无向图。边是顶点的无序对,记为(v, w)或(w,v),因为

(v,w)=(w,v), 其中v,w是顶点。可以说顶点w和顶点v互为邻接点。边(v, w)依附于顶点w和v,或者说

边(v, w)和顶点v, w相关联。

图(b)所示的无向图G2可表示为

3. 简单图

一个图G若满足:

① 不存在重复边;

② 不存在顶点到自身的边

则称图G为简单图。

上图中G 1 和G 2均为简单图。数据结构中仅讨论简单图。

4. 多重图

若图G中某两个结点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则G为多重图。

多重图的定义和简单图是相对的。

5. 完全图(也称简单完全图)

对于无向图,∣E∣的取值范围是0到n ( n − 1 ) / 2,有n ( n − 1 ) / 2 条边的无向图称为完全图,在完

全图中任意两个顶点之间都存在边。对于有向图,∣E∣的取值范围是0到n ( n − 1 ) ,有n ( n − 1 )

条弧的有向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。上图

中G2 为无向完全图,而G3为有向完全图。

6. 子图

设有两个图G = ( V , E ) 和G ′ = ( V ′ , E ′ ) ′, 若V ′ 是V的子集,且E ′是E的子集,则称G ′ 是G的

子图。若有满足V ( G ′ ) = V ( G ) 的子图G′,则称其为G的生成子图。上图中G3 为G1 的子图。

注意:并非V和E的任何子集都能构成G的子图,因为这样的子集可能不是图,即E的子集中的某些

边关联的顶点可能不在这个V的子集中。

7. 连通、连通图和连通分量

在无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的。若图G中任意两个顶点都是连

通的,则称图G为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量。若一个图

有n个顶点,并且边数小于n −1,则此图必是非连通图。如下图(a)所示, 图G4有3个连通分量,如

图(b)所示。

注意:弄清连通、连通图、连通分量的概念非常重要。首先要区分极大连通子图和极小连通子图,

极大连通子图是无向图的连通分量,极大即要求该连通子图包含其所有的边;极小连通子图是既要

保持图连通又要使得边数最少的子图。

8. 强连通图、强连通分量

在有向图中,若从顶点v到顶点w和从顶点w到项点v之间都有路径,则称这两个顶点是强连通的。若

图中任何一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的

强连通分量,图G 1 的强连通分量如下图所示。

注意:强连通图、强连通分量只是针对有向图而言的。一般在无向图中讨论连通性,在有向图中考

虑强连通性。

9. 生成树、生成森林

连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为n,则它的生成树含有n

− 1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回

路。在非连通图中,连通分量的生成树构成了非连通图的生成森林。图G2的一个生成树如下图所

示。

注意:包含无向图中全部顶点的极小连通子图,只有生成树满足条件,因为砍去生成树的任一条

边,图将不再连通。

10. 顶点的度、入度和出度

图中每个顶点的度定义为以该项点为一个端点的边的数目。

对于无向图,顶点v的度是指依附于该顶点的边的条数,记为TD(v)。

在具有n个顶点、e条边的无向图中

即无向图的全部顶点的度的和等于边数的2倍,因为每条边和两个顶点相关联。

对于有向图,顶点v的度分为入度和出度,入度是以顶点v为终点的有向边的数目,记为ID(v) ; 而出度

是以顶点v为起点的有向边的数目,记为OD(v)。顶点v的度等于其入度和出度之和,即TD(v) = ID

(v) + OD (v) 。

在具有n个顶点、e条边的有向图中

即有向图的全部顶点的入度之和与出度之和相等,并且等于边数。这是因为每条有向边都有一个起

点和终点。

11. 边的权和网

在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值

的图称为带权图,也称网。

12. 稠密图、稀疏图

边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常

是相对而言的。一般当图G满足∣E∣ < ∣V∣ log∣V∣ 时,可以将G视为稀疏图。

13. 路径、路径长度和回路

顶点vp 到顶点vq 之间的一条路径是指顶点序列vp , vi1 , vi2 , . . . , vim , vq ,当然关联的边也可以

理解为路径的构成要素。路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称

为回路或环。若一个图有n个顶点,并且有大于n − 1条边,则此图一定有环。

14. 简单路径、简单回路

在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点

不重复出现的回路称为简单回路。

15. 距离

从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存

在路径,则记该距离为无穷( ∞ ) (∞)(∞)。

16. 有向树

一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。

五、图的存储结构

由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理

位置来表示元素之间的关系,也就是说,图不可能用简单的顺序存储结构来表示。而多重链表的方

式,要么会造成很多存储单元的浪费,要么又带来操作的不便。因此,对于图来说,如何对它实现

物理存储是个难题,接下来我们介绍五种不同的存储结构。

1. 邻接矩阵

图的邻接矩阵(Adjacency Matrix) 存储方式是用两个数组来表示图。一个一维数组存储图中顶点信

息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

设图G GG有n nn个顶点,则邻接矩阵A AA是一个n ∗ n n*n n∗n的方阵,定义为:

下图是一个无向图和它的邻接矩阵:

可以看出:

  1. 无向图的邻接矩阵一定是一个对称矩阵(即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的)。 因此,在实际存储邻接矩阵时只需存储上(或下)三角矩阵的元素。
  2. 对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度TD(vi) 。比如顶点v1 的度就是1 + 0 + 1 + 0 = 2。

求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍, A[i] [j] 为 1就是邻接点。

下图是有向图和它的邻接矩阵:

可以

  1. 主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称。
  2. 有向图讲究入度与出度,顶点v1的入度为1,正好是第v1列各数之顶点v1 的出度为2,即第v 1v行的各数
  3. 与无向图同样的办法,判断顶点vi 到vj 是否存在弧,只需要查找矩阵中A [i] [j] 是否为1

对于带权图而言,若顶点vi和vj 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值

下图是有向网图和它的邻接矩阵:

通过以上对无向图、有向图和网的描述,可定义出邻接矩阵的存储结构:

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

public class AdjacencyMatrix {

    private ArrayList<String> vexs; // 顶点表
    private int[][] edges; // 边表
    int numVertexes;
    int numEdges;
    boolean[] visited;

    public AdjacencyMatrix(int numVertexes, int numEdges) {
        this.numVertexes = numVertexes;
        this.numEdges = numEdges;
        this.vexs = new ArrayList<String>(numVertexes);
        this.edges = new int[numVertexes][numVertexes];
        this.visited = new boolean[numVertexes];
    }

    private void insertVex(String v) {
        vexs.add(v);
    }

    private void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
    }

    private void show() {
        for (int[] link : edges) {
            System.out.println(Arrays.toString(link));
        }
    }

    private void DFS(int i) {
        visited[i] = true;
        System.out.print(vexs.get(i) + " ");
        for (int j = 0; j < numVertexes; j++) {
            if (edges[i][j] > 0 && !visited[j]) {
                DFS(j);
            }
        }
    }

    private void DFSTraverse() {
        int i;
        for (i = 0; i < numVertexes; i++) {
            visited[i] = false;
        }
        for (i = 0; i < numVertexes; i++) {
            if (!visited[i]) {
                DFS(i);
            }
        }
    }

    private void BFSTraverse() {
        int i, j;
        LinkedList queue = new LinkedList();
        for (i = 0; i < numVertexes; i++) {
            visited[i] = false;
        }
        for (i = 0; i < numVertexes; i++) {
            if (!visited[i]) {
                visited[i] = true;
                System.out.print(vexs.get(i) + " ");
                queue.addLast(i);
                while (!queue.isEmpty()) {
                    i = (Integer) queue.removeFirst();
                    for (j = 0; j < numVertexes; j++) {
                        if (edges[i][j] > 0 && !visited[j]) {
                            visited[j] = true;
                            System.out.print(vexs.get(j) + " ");
                            queue.addLast(j);
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        int numVertexes = 9;
        int numEdges = 15;
        AdjacencyMatrix graph = new AdjacencyMatrix(numVertexes, numEdges);
        graph.insertVex("A");
        graph.insertVex("B");
        graph.insertVex("C");
        graph.insertVex("D");
        graph.insertVex("E");
        graph.insertVex("F");
        graph.insertVex("G");
        graph.insertVex("H");
        graph.insertVex("I");
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 5, 1);
        graph.insertEdge(1, 2, 1);
        graph.insertEdge(1, 6, 1);
        graph.insertEdge(1, 8, 1);
        graph.insertEdge(2, 3, 1);
        graph.insertEdge(2, 8, 1);
        graph.insertEdge(3, 4, 1);
        graph.insertEdge(3, 6, 1);
        graph.insertEdge(3, 7, 1);
        graph.insertEdge(3, 8, 1);
        graph.insertEdge(4, 7, 1);
        graph.insertEdge(4, 5, 1);
        graph.insertEdge(5, 6, 1);
        graph.insertEdge(6, 7, 1);
        System.out.println("邻接矩阵");
        graph.show();
        System.out.print("深度优先遍历:");
        graph.DFSTraverse();
        System.out.println();
        System.out.print("广度优先遍历:");
        graph.BFSTraverse();
    }
    
}

注意:

① 在简单应用中,可直接用二维数组作为图的邻接矩阵(顶点信息等均可省略)。

② 当邻接矩阵中的元素仅表示相应的边是否存在时,EdgeType可定义为值为0和1的枚举类型。

③ 无向图的邻接矩阵是对称矩阵,对规模特大的邻接矩阵可采用压缩存储。

④ 邻接矩阵表示法的空间复杂度为O ( n 2 ),其中n为图的顶点数∣V |。

⑤ 用邻接矩阵法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有

多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。

⑥ 稠密图适合使用邻接矩阵的存储表示。

2. 邻接表

当一个图为稀疏图时(边数相对顶点较少),使用邻接矩阵法显然要浪费大量的存储空间,如下图所示:

而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。

所谓邻接表,是指对图G中的每个顶点v i建立一个单链表,第i个单链表中的结点表示依附于顶点vi

的边(对于有向图则是以顶点v i为尾的弧),这个单链表就称为顶点v i的边表(对于有向图则称为出边

表)。边表的头指针和顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:

顶点表结点和边表结点,如下图所示。

顶点表结点由顶点域(data)和指向第一条邻接边的指针(firstarc) 构成,边表(邻接表)结点由邻接点

域(adjvex)和指向下一条邻接边的指针域(nextarc) 构成。

无向图的邻接表的实例如下图所示。

有向图的邻接表的实例如下图所示。

此时我们很容易就可以算出某个顶点的入度或出度是多少,判断两顶点是否存在弧也很容易实现。

对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。

图的邻接表存储结构定义如下:

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

class EdgeNode {
    int vex;
    int adjvex; // 邻接点域,存储该顶点对应的下标
    int weight;
    EdgeNode next;

    public EdgeNode(int vex, int adjvex, int weight) {
        this.vex = vex;
        this.adjvex = adjvex;
        this.weight = weight;
        this.next = null;
    }
}

class VertexNode {
    String data; // 顶点域,存储顶点信息
    EdgeNode firstedge; // 边表头

    public VertexNode(String data) {
        this.data = data;
        this.firstedge = null;
    }
}

public class AdjacencyList {

    private ArrayList<VertexNode> vexs; // 顶点表
    int numVertexes;
    int numEdges;
    boolean[] visited;

    public AdjacencyList(int numVertexes, int numEdges) {
        this.numVertexes = numVertexes;
        this.numEdges = numEdges;
        this.vexs = new ArrayList<VertexNode>(numVertexes);
        this.visited = new boolean[numVertexes];
    }

    private void insertVex(VertexNode v) {
        vexs.add(v);
    }

    private void insertEdge(EdgeNode e) {
        int i = e.vex; // 顶点表中对应结点的下标
        int j = e.adjvex; // 边表结点对应的下标
        VertexNode vexi = vexs.get(i);
        VertexNode vexj = vexs.get(j);
        e.next = vexi.firstedge;
        vexi.firstedge = e;

        EdgeNode e2 = new EdgeNode(j, i, 1);
        e2.next = vexj.firstedge;
        vexj.firstedge = e2;
    }

    private void show() {
        for (int i = 0; i < numVertexes; i++) {
            VertexNode vex = vexs.get(i);
            System.out.print("【" + vex.data + "】--->");
            EdgeNode node = vex.firstedge;
            while (node != null) {
                System.out.print(vexs.get(node.adjvex).data + "(" + node.adjvex + ")" + "->");
                node = node.next;
            }
            System.out.print("null");
            System.out.println();
        }
    }

    private void DFS(int i) {
        EdgeNode p;
        visited[i] = true;
        System.out.print(vexs.get(i).data + " ");
        p = vexs.get(i).firstedge;
        while (p != null) {
            if (!visited[p.adjvex]) {
                DFS(p.adjvex);
            }
            p = p.next;
        }
    }

    private void DFSTraverse() {
        int i;
        for (i = 0; i < numVertexes; i++) {
            visited[i] = false;
        }
        for (i = 0; i < numVertexes; i++) {
            if (!visited[i]) {
                DFS(i);
            }
        }
    }

    private void BFSTraverse() {
        EdgeNode p;
        int i;
        LinkedList queue = new LinkedList();
        for (i = 0; i < numVertexes; i++) {
            visited[i] = false;
        }
        for (i = 0; i < numVertexes; i++) {
            if (!visited[i]) {
                visited[i] = true;
                System.out.print(vexs.get(i).data + " ");
                queue.addLast(i);
                while (!queue.isEmpty()) {
                    i = (Integer) queue.removeFirst();
                    p = vexs.get(i).firstedge;
                    while (p != null) {
                        if (!visited[p.adjvex]) {
                            visited[p.adjvex] = true;
                            System.out.print(vexs.get(p.adjvex).data + " ");
                            queue.addLast(p.adjvex);
                        }
                        p = p.next;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        int numVertexes = 9;
        int numEdges = 15;
        AdjacencyList graph = new AdjacencyList(numVertexes, numEdges);
        graph.insertVex(new VertexNode("A"));
        graph.insertVex(new VertexNode("B"));
        graph.insertVex(new VertexNode("C"));
        graph.insertVex(new VertexNode("D"));
        graph.insertVex(new VertexNode("E"));
        graph.insertVex(new VertexNode("F"));
        graph.insertVex(new VertexNode("G"));
        graph.insertVex(new VertexNode("H"));
        graph.insertVex(new VertexNode("I"));
        graph.insertEdge(new EdgeNode(0, 1, 1));
        graph.insertEdge(new EdgeNode(0, 5, 1));
        graph.insertEdge(new EdgeNode(1, 2, 1));
        graph.insertEdge(new EdgeNode(1, 6, 1));
        graph.insertEdge(new EdgeNode(1, 8, 1));
        graph.insertEdge(new EdgeNode(2, 3, 1));
        graph.insertEdge(new EdgeNode(2, 8, 1));
        graph.insertEdge(new EdgeNode(3, 4, 1));
        graph.insertEdge(new EdgeNode(3, 6, 1));
        graph.insertEdge(new EdgeNode(3, 7, 1));
        graph.insertEdge(new EdgeNode(3, 8, 1));
        graph.insertEdge(new EdgeNode(4, 7, 1));
        graph.insertEdge(new EdgeNode(4, 5, 1));
        graph.insertEdge(new EdgeNode(5, 6, 1));
        graph.insertEdge(new EdgeNode(6, 7, 1));
        System.out.println("邻接表");
        graph.show();
        System.out.print("深度优先遍历:");
        graph.DFSTraverse();
        System.out.println();
        System.out.print("广度优先遍历:");
        graph.BFSTraverse();
    }

}

图的邻接表存储方法具有以下特点

  1. 若G为无向图,则所需的存储空间为O ( ∣V∣ + 2 ∣E∣ ) ;若G GG为有向图,则所需的存储空间为O (∣V∣ + ∣E∣ ) 。前者的倍数2是由于无向图中,每条边在邻接表中出现了两次。
  2. 对于稀疏图,采用邻接表表示将极大地节省存储空间。
  3. 在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为O ( n ) 。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低。
  4. 在有向图的邻接表表示中,求一个给定顶点的出度只需计算其邻接表中的结点个数;但求其顶点的入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度。当然,这实际上与邻接表存储方式是类似的。
  5. 图的邻接表表示并不唯一,因为在每个顶点对应的单链表中,各边结点的链接次序可以是任意的,它取决于建立邻接表的算法及边的输入次序。

3. 十字链表

十字链表是有向图的一种链式存储结构。

对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才能知

道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可能把邻接表与逆邻接表结合起来

呢?答案是肯定的,就是把它们整合在一起。这就是我们现在要介绍的有向图的一种存储方法:十

字链表(Orthogonal List)。

我们重新定义顶点表结点结构如下表所示。

其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout 表示出边表头指针,指

向该顶点的出边表中的第一个结点。重新定义的边表结点结构如下表所示。

其中tailvex 是指弧起点在顶点表的下标,headvex 是指弧终点在顶点表中的下标,headlink是指入

边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果

是网,还可以再增加一个weight域来存储权值。

接下来通过一个例子详细介绍十字链表的结构。

如下图所示,顶点依然是存入一个一维数组{ V0, V1 , V2, V3} ,实线箭头指针的图示完全与的邻接

表的结构相同。就以顶点V0 来说,firstout 指向的是出边表中的第一个结点V3 。所以V0 边表结点

的headvex = 3 ,而tailvex就是当前顶点V0 的下标0,由于V0只有一个出边顶点,所以headlink和

taillink都是空。

我们重点需要来解释虚线箭头的含义,它其实就是此图的逆邻接表的表示。

对于V0 来说,它有两个顶点V1 和V2的入边。

因此V0 的firstin指向顶点V1 的边表结点中headvex为0的结点,如上图右图中的①。

接着由入边结点的

headlink指向下一个入边顶点V2 ,如图中的②。

对于顶点V1 ,它有一个入边顶点V2 ,所以它的firstin指向顶点

V2 的边表结点中headvex为1的结点,如图中的③。

顶点V2 和V3 也是同样有一个入边顶点,如图中④和⑤。

十字链表的好处就是因为把邻接表和逆邻接表整合在了一起, 这样既容易找到以V1 为尾的弧,也

容易找到以V!

为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外,其实创建图算法的时间

复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表是非常好的数据结构模型。

import java.util.ArrayList;

/**
 * 十字链表
 * @author zhengge
 * @version 1.0
 * @create 2024/5/25 16:43
 */
public class CrossLinkedListGraph<E> {


    class Vertex{
        E data;
        Edge firstIn;
        Edge firstOut;

        public Vertex(E data, Edge firstIn, Edge firstOut) {
            this.data = data;
            this.firstIn = firstIn;
            this.firstOut = firstOut;
        }
    }
    class Edge{
        int fromVertex;
        int toVertex;
        Edge fromEdge;
        Edge toEdge;
        int weight;

        public Edge(int fromVertex, int toVertex, Edge fromEdge, Edge toEdge, int weight) {
            this.fromVertex = fromVertex;
            this.toVertex = toVertex;
            this.fromEdge = fromEdge;
            this.toEdge = toEdge;
            this.weight = weight;
        }
    }
    private int numOfVertices;
    private int maxOfVertices;
    private ArrayList<Vertex> vertices;

    public CrossLinkedListGraph(int maxOfVertices) {
        this.maxOfVertices = maxOfVertices;
        this.vertices = new ArrayList<>(maxOfVertices);
        numOfVertices = 0;
    }


    public boolean putVertex(E data){
        if (numOfVertices < maxOfVertices){
            vertices.add(new Vertex(data,null,null));
            numOfVertices ++;
            return true;
        }
         return false;
    }
    public E getVertexData(int vertexIndex){
        if (vertexIndex<maxOfVertices){
            return vertices.get(vertexIndex).data;
        }
        return null;
    }
    /**
     * 插入边,横纵列表都要插入
     */
    public boolean putEdge(int fromVertexIndex,int toVertexIndex,int weight){
        if (fromVertexIndex < maxOfVertices && toVertexIndex < maxOfVertices){
            Vertex fromVertex = vertices.get(fromVertexIndex);
            Edge newEdge = new Edge(fromVertexIndex,toVertexIndex,null,null,weight);
            //插入横向链表
            if (fromVertex.firstOut == null) {
                fromVertex.firstOut = newEdge;
                //插入竖向链表
                return insertFromEdgeLinkedList(fromVertex.firstOut);
            }
            //遍历元素然后将元素放到尾部
            Edge edge = fromVertex.firstOut;
            while (edge.toEdge != null) {
                edge = edge.toEdge;
            }
            edge.toEdge = newEdge;
            //插入竖向链表
            return insertFromEdgeLinkedList(edge.toEdge);
        }
        return false;
    }

    /**
     * 将插入竖向链表提升为一个方法
     */
    private boolean insertFromEdgeLinkedList(Edge edge){
        if ( edge!=null){
            //获得新增加的指向的顶点
            Vertex toVertex = vertices.get(edge.toVertex);
            if (toVertex.firstIn==null){
                //如果指向的顶点没有竖向链表就直接将第一个边赋值给竖向链表
                toVertex.firstIn = edge;
                return true;
            }
            Edge fromEdge = toVertex.firstIn;
            while (fromEdge.fromEdge!=null){
                fromEdge = fromEdge.fromEdge;
            }
            fromEdge.fromEdge = edge;
            return true;
        }
        return  false;
    }

    public void print(){
        for (Vertex vertex : vertices) {
            Edge edge = vertex.firstOut;
            System.out.print(vertex.data+": ");
            while (edge!=null){
                System.out.print(vertices.get(edge.fromVertex).data
                        + " --> "
                        +vertices.get(edge.toVertex).data
                        +" weight="+edge.weight +"; ");
                edge = edge.toEdge ;
            }
            System.out.println();
        }
    }
    public void printVertexFromEdge(int fromVertex){
        System.out.println("====print vertex from edges====");
        Vertex vertex = vertices.get(fromVertex);
        Edge firstIn = vertex.firstIn;
        if (vertex.firstIn!=null){
            Edge fromEdge = vertex.firstIn;
             while (fromEdge!=null){//这里debug的时候录为null
                 System.out.println(vertices.get(fromEdge.fromVertex).data
                         +" --> "+
                         vertices.get(fromEdge.toVertex).data);
                 fromEdge = fromEdge.fromEdge;
             }
        }else {
            System.out.println("null of firstIn");
        }
    }


}

4. 邻接多重表

邻接多重表是无向图的另一种链式存储结构。

在邻接表中,容易求得顶点和边的各种信息,但在邻接表中求两个顶点之间是否存在边而对边执行

删除等操作时,需要分别在两个顶点的边表中遍历,效率较低。比如下图中,若要删除左图的( V0

, V2 ) 这条边,需要对邻接表结构中右边表的阴影两个结点进行删除操作,显然这是比较烦琐的。

重新定义的边表结点结构如下表所示。

其中ivex和jvex是与某条边依附的两个顶点在顶点表中下标。ilink 指向依附顶点ivex的下一条边,

jlink 指向依附顶点jvex的下一条边。这就是邻接多重表结构。

每个顶点也用一一个结点表示,它由如下所示的两个域组成。

其中,data 域存储该顶点的相关信息,firstedge 域指示第一条依附于该顶点的边。

我们来看结构示意图的绘制过程,理解了它是如何连线的,也就理解邻接多重表构造原理了。如下

图7所示,左图告诉我们它有4个顶点和5条边,显然,我们就应该先将4个顶点和5条边的边表结点

画出来。

我们开始连线,如图,首先连线的①②③④就是将顶点的firstedge指向一条边,顶点下标要与ivex

的值相同,这很好理解。

接着,由于顶点V0 的( V0 , V1 )边的邻边有(V0 , V3) 和( V 0 , V 2 )。

因此⑤⑥的连线就是满足指向下一条依附于顶点V0的边的目标,注意ilink指向的结点的jvex一定要

和它本身的ivex的值相同。

同样的道理,连线⑦就是指(V1 , V0 )这条边,它是相当于顶点V1指向(V1,V2 ) 边后的下一条。

V2有三条边依附,所以在③之后就有了⑧⑨。

连线④的就是顶点V3在连线④之后的下一条边。

左图一共有5条边,所以右图有10条连线,完全符合预期。

到这里,可以明显的看出,邻接多重表与邻接表的差别,仅仅是在于同一条边在邻接表中用两个结

点表示,而在邻接多重表中只有一个结点。

这样对边的操作就方便多了,若要删除左图的( V0 , V2) 这条边,只需要将右图的⑥⑨的链接指向

改为NULL即可。

import java.util.LinkedList;
 
public class GraphAdjacencyList {
 
    // 使用LinkedList来存储邻接节点,这里假设图是无向的
    private LinkedList<Integer>[] adjacencyList;
 
    public GraphAdjacencyList(int vertices) {
        adjacencyList = new LinkedList[vertices];
        for (int i = 0; i < vertices; i++) {
            adjacencyList[i] = new LinkedList<>();
        }
    }
 
    // 添加边的方法
    public void addEdge(int source, int destination) {
        adjacencyList[source].add(destination);
        adjacencyList[destination].add(source); // 因为是无向图,所以需要添加到两个节点
    }
 
    // 打印邻接表
    public void printAdjacencyList() {
        for (int vertex = 0; vertex < adjacencyList.length; vertex++) {
            System.out.print(vertex + " -> ");
            for (int neighbor : adjacencyList[vertex]) {
                System.out.print(neighbor + " ");
            }
            System.out.println();
        }
    }
 
    public static void main(String[] args) {
        GraphAdjacencyList graph = new GraphAdjacencyList(5);
        graph.addEdge(1, 0);
        graph.addEdge(0, 2);
        graph.addEdge(0, 3);
        graph.addEdge(3, 4);
        graph.printAdjacencyList();
    }
}

5. 边集数组

边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每

个数据元素由一条边的起点下标(begin)、 终点下标(end)和权(weight)组成,如下图所示。显然边

集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。

因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。

package E;

import java.util.Arrays;
import java.util.Scanner;

public class A {
    private static Scanner sc = new Scanner(System.in);
    private static final int N = 1000;
    private static int n, m, cnt;
    private static String vex[] = new String[N];

    private static class Edge {
        int u;
        int v;
        int w;
    }

    private static Edge e[] = new Edge[N * N];

    private static void init() {
        Arrays.fill(vex, -1);
        Arrays.fill(e, -1);
        cnt = 0;
    }

    private static int locateVex(String x) {
        for (int i = 0; i < n; i++) {
            if (vex[i].equals(x)) {
                return i;
            }
        }
        return -1;
    }

    private static void add(int i, int j, int w) {
        e[cnt] = new Edge();
        e[cnt].u = i;
        e[cnt].v = j;
        e[cnt++].w = w;
    }

    private static void createGraph() {
        String u, v;
        int w;
        while (m-- > 0) {
            u = sc.next();
            v = sc.next();
            w = sc.nextInt();
            int i = locateVex(u);
            int j = locateVex(v);
            if (i != -1 && j != -1) {
                add(i, j, w);
            } else {
                System.out.println("错误");
                m++;
            }
        }
    }

    private static void print() {
        System.out.println("边集数组如下:");
        for (int i = 0; i < cnt; i++) {
            System.out.println(e[i].u + " " + e[i].v + " " + e[i].w);
        }
    }

    public static void main(String[] args) {
        n = sc.nextInt();
        m = sc.nextInt();
        for (int i = 0; i < n; i++) {
            vex[i] = sc.next();
        }

        createGraph();
        print();

    }
}
/*
4 6
A B C D
A B 3
A C 5
B C 2
C B 6
C D 2
D B 7
 */

六、图的遍历

图的遍历是和树的遍历类似,我们希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,

这一过程就叫做图的遍历(Traversing Graph)。

对于图的遍历来,通常有两种遍历次序方案:它们是深度优先遍历和广度优先遍历。

1. 深度优先遍历

深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。

1.1. DFS算法

深度优先搜索类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的搜索策

略是尽可能"深"地搜索一个图。

它的基本思想如下:首先访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一

顶点w1 ,再访问与w1 邻接且未被访问的任一顶点...重复上述过程。

当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该

点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。一般情况下,其递归形式的算法十

分简洁,算法过程如下:

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

// 用于表示图的节点
class Node {
    int value;
    List<Node> neighbors;

    public Node(int value) {
        this.value = value;
        neighbors = new ArrayList<>();
    }
}

public class DepthFirstSearch {
    public static void dfs(Node start) {
        Stack<Node> stack = new Stack<>();
        stack.push(start);

        while (!stack.isEmpty()) {
            Node current = stack.pop();
            if (current != null && !current.visited) {
                current.visited = true;
                System.out.print(current.value + " ");

                // 将当前节点的未访问过的邻居节点入栈
                for (Node neighbor : current.neighbors) {
                    if (!neighbor.visited) {
                        stack.push(neighbor);
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        // 构造图
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);
        Node node5 = new Node(5);

        node1.neighbors.add(node2);
        node1.neighbors.add(node3);
        node2.neighbors.add(node4);
        node3.neighbors.add(node4);
        node4.neighbors.add(node5);

        System.out.println("深度优先搜索结果:");
        dfs(node1);
    }
}

以下面这个无向图为例

其深度优先遍历的结果为a b d e h c f g abdehcfgabdehcfg

1.2. DFS算法的性能分析

DFS算法是一个递归算法,需要借助一个递归工作栈,故其空间复杂度为O ( V ) O(V)O(V)。

对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中

的所有元素,因此都需要O ( V 2 ) O(V^2)O(V2)的时间。而邻接表做存储结构时,找邻接点所需的

时间取决于顶点和边的数量,所以是O ( V + E ) O(V+E)O(V+E)。 显然对于点多边少的稀疏图来

说,邻接表结构使得算法在时间效率上大大提高。

对于有向图而言,由于它只是对通道存在可行或不可行,算法上没有变化,是完全可以通用的。

1.3. 深度优先的生成树和生成森林

深度优先搜索会产生一棵深度优先生成树。

当然,这是有条件的,即对连通图调用DFS才能产生深度优先生成树,否则产生的将是深度优先生

成森林,如下图所示。

基于邻接表存储的深度优先生成树是不唯一的 。

2. 广度优先遍历

广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。

2.1. BFS算法

如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。

广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有

往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,

以记忆正在访问的顶点的下一层顶点。以下是广度优先遍历的代码:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

// 用于表示图的节点
class Node {
    int value;
    List<Node> neighbors;

    public Node(int value) {
        this.value = value;
        neighbors = new ArrayList<>();
    }
}

public class BreadthFirstSearch {
    public static void bfs(Node start) {
        Queue<Node> queue = new LinkedList<>();
        queue.offer(start);
        start.visited = true;

        while (!queue.isEmpty()) {
            Node current = queue.poll();
            System.out.print(current.value + " ");

            // 将当前节点的未访问过的邻居节点入队列
            for (Node neighbor : current.neighbors) {
                if (!neighbor.visited) {
                    queue.offer(neighbor);
                    neighbor.visited = true;
                }
            }
        }
    }

    public static void main(String[] args) {
        // 构造图
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);
        Node node5 = new Node(5);

        node1.neighbors.add(node2);
        node1.neighbors.add(node3);
        node2.neighbors.add(node4);
        node3.neighbors.add(node4);
        node4.neighbors.add(node5);

        System.out.println("广度优先搜索结果:");
        bfs(node1);
    }
}

以下面这个无向图为例

其广度优先遍历的结果为a b c d e f g h abcdefghabcdefgh。

2.2. BFS算法性能分析

无论是邻接表还是邻接矩阵的存储方式,BFS 算法都需要借助一个辅助队列Q, n个顶点均需入队一

次,在最坏的情况下,空间复杂度为O (V) 。

采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次), 在搜索任一顶点的邻接点时,每条

边至少访问一次,算法总的时间复杂度为O ( V + E ) 。采用邻接矩阵存储方式时,查找每个顶点的

邻接点所需的时间为O ( V ),故算法总的时间复杂度为O ( V2 ) 。

注意:图的邻接矩阵表示是唯一的,但对于邻接表来说,若边的输入次序不同,生成的邻接表也不

同。因此,对于同样一个图,基于邻接矩阵的遍历所得到的DFS序列和BFS序列是唯一的,基于邻

接表的遍历所得到的DFS序列和BFS序列是不唯一的。

3. 图的遍历与图的连通性

图的遍历算法可以用来判断图的连通性。

对于无向图来说,若无向图是连通的,则从任一结点出发, 仅需一次遍历就能够访问图中的所有

顶点;若无向图是非连通的,则从某一个顶点出发,一次遍历只能访问到该顶点所在连通分量的所

有顶点,而对于图中其他连通分量的顶点,则无法通过这次遍历访问。对于有向图来说,若从初始

点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,否则不能访问到所有顶点。

故在BFSTraverse ()或DFSTraverse ()中添加了第二个for循环,再选取初始点,继续进行遍历,以

防止一次无法遍历图的所有顶点。

对于无向图,上述两个函数调用BFS (G,i)或DFS(G,i)的次数等于该图的连通分量数;而对于有向

图则不是这样,因为一个连通的有向图分为强连通的和非强连通的,它的连通子图也分为强连通分

量和非强连通分量,非强连通分量一次调用BFS (G, i)或DFS (G, i)无法访问到该连通分量的所有顶

点。

如下图所示为有向图的非强连通分量。

最小生成树

一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n

− 1 n-1n−1条边,若砍去它的一条边,则会使生成树变成非连通图;若给它增加一条边,则会形成

图中的一条回路。

对于一个带权连通无向图G = ( V , E ) G=(V, E)G=(V,E),生成树不同,其中边的权值之和最小的那

棵生成树(构造连通网的最小代价生成树),称为G的最小生成树(Minimum-Spanning-Tree,

MST)。

构造最小生成树有多种算法,但大多数算法都利用了最小生成树的下列性质:假设G = ( V , E )

G=(V, E)G=(V,E)是一个带权连通无向图,U UU是顶点集V VV的一个非空子集。若( u , v ) (u,v)

(u,v)是一条具有最小权值的边,其中u ∈ U , v ∈ V − U u∈U,v∈V-Uu∈U,v∈V−U,则必存在一

棵包含边( u , v ) (u, v)(u,v)的最小生成树。

基于该性质的最小生成树算法主要有Prim算法和Kruskal算法,它们都基于贪心算法的策略。

下面介绍一个通用的最小生成树算法:

通用算法每次加入一条边以逐渐形成一棵生成树,下面介绍两种实现上述通用算法的途径。

3.1. 普里姆(Prim)算法

Prim算法构造最小生成树的过程如下图所示。

初始时从图中任取一顶点(如顶点加入树T,此时树中只含有一个顶点,之后选择一个与当前T中顶

点集合距离最近的顶点,并将该顶点和相应的边加入T,每次操作后T中的顶点数和边数都增1。

以此类推,直至图中所有的顶点都并入T,得到的T就是最小生成树。此时T中必然有n-1条边。

通俗点说就是:从一个顶点出发,在保证不形成回路的前提下,每找到并添加一条最短的边,就把

当前形成的连通分量当做一个整体或者一个点看待,然后重复"找最短的边并添加"的操作。

Prim算法的步骤如下:

假设G = { V , E } 是连通图,其最小生成树T = ( U , ET ) ,ET 是最小生成树中边的集合。

初始化:向空树T = ( U , ET ) 中添加图G = ( V , E )的任一顶点u0 ,使U = { u0 } ,ET = N U L。

循环(重复下列操作直至U = V ):从图G中选择满足{ ( u , v ) ∣ u ∈ U , v ∈ V − U } 且具有最小权

值的边( u , v ) ,加入树T,置U = U ∪ { v } ,ET = ET U { ( u , v ) } 。

额,不得不说这样理解起来有点抽象,为了能描述这个算法,我们先构造一个邻接矩阵,如下图的

右图所示。

于是普里姆(Prim) 算法代码如下,左侧数字为行号。其中INFINITY为权值极大值,不妨设65535,

MAXVEX 为顶点个数最大值,此处大于等于9即可。

import java.util.*;
 
public class PrimAlgorithm {
    public static void main(String[] args) {
        int vertices = 5; // 图中顶点数量
        
        // 创建无向连通图的邻接矩阵表示
        int graph[][] = {
  
  {0, 2, 0, 6, 4},
                          {2, 0, 3, 8, 0},
                          {0, 3, 0, 1, 7},
                          {6, 8, 1, 0, 2},
                          {4, 0, 7, 2, 0}};
        
        // 调用prim函数计算最小生成树并输出结果
        List<Integer> minSpanningTree = prim(graph);
        
        System.out.println("最小生成树边集合为:" + minSpanningTree);
    }
    
    private static List<Integer> prim(int graph[][]) {
        boolean visited[] = new boolean[graph.length]; // 记录节点是否已访问过
        int parent[] = new int[graph.length]; // 存放每个节点在最小生成树上的前驱节点
        int key[] = new int[graph.length]; // 存放当前节点到最小生成树上其他节点的最小权值
        List<Integer> minSpanningTree = new ArrayList<>(); // 保存最小生成树的边集合
        
        Arrays.fill(visited, false); // 初始化所有节点未被访问过
        for (int i = 1; i < graph.length; ++i) {
            parent[i] = -1; // 将所有节点的前驱节点设置为-1(没有前驱)
            key[i] = Integer.MAX_VALUE; // 将所有节点与最小生成树之间的权值设置为正无穷大
        }
        
        key[0] = 0; // 第一个节点不需要特殊处理,直接标记为已访问且权值为0
        while (!isAllVisited(visited)) {
            int u = findMinKeyVertex(key, visited); // 选取权值最小的未访问节点u
            
            if (parent[u] != -1 && !visited[parent[u]]) {
                minSpanningTree.add(parent[u]); // 如果u的前驱节点还未访问过,则加入最小生成树的边集合
            }
            
            visited[u] = true; // 标记节点u为已访问
            
            updateKeys(graph, key, visited, u); // 更新与节点u相关联的节点的权值
        }
        
        return minSpanningTree;
    }
    
    private static int findMinKeyVertex(int key[], boolean visited[]) {
        int minIndex = -1;
        int minValue = Integer.MAX_VALUE;
        
        for (int v = 0; v < key.length; ++v) {
            if (!visited[v] && key[v] <= minValue) {
                minValue = key[v];
                minIndex = v;
            }
        }
        
        return minIndex;
    }
    
    private static void updateKeys(int graph[][], int key[], boolean visited[], int u) {
        for (int v = 0; v < graph.length; ++v) {
            if (!visited[v] && graph[u][v] > 0 && graph[u][v] < key[v]) {
                key[v] = graph[u][v];
                parent[v] = u;
            }
        }
    }
    
    private static boolean isAllVisited(boolean visited

由算法代码中的循环嵌套可得知此算法的时间复杂度为O ( n 2 ) O(n^2)O(n2)。

3.2. 克鲁斯卡尔(Kruskal)算法

与Prim算法从顶点开始扩展最小生成树不同,Kruskal 算法是一种按权值的递增次序选择合适的边

来构造最小生成树的方法。

Kruskal算法构造最小生成树的过程如下图所示。初始时为只有n个顶点而无边的非连通图T = V ,每

个顶点自成一个连通分量,然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最

小的边,若该边依附的顶点落在T中不同的连通分量上,则将此边加入T ,否则舍弃此边而选择下

一条权值最小的边。以此类推,直至T中所有顶点都在一个连通分量上。

算法思路:

我们可以直接就以边为目标去构建,因为权值是在边上,直接去找最小权值的边来构建生成树也是

很自然的想法,只不过构建时要考虑是否会形成环路而已。此时我们就用到了图的存储结构中的边

集数组结构。以下是edge边集数组结构的定义代码:

我们将下面左图的邻接矩阵通过程序转化为右图的边集数组,并且对它们按权值从小到大排序。

于是Kruskal算法代码如下,左侧数字为行号。其中MAXEDGE为边数量的极大值,此处大于等于

15即可,MAXVEX为顶点个数最大值,此处大于等于9即可。

import java.util.*;

public class KruskalAlgorithm {
    public static List<Edge> kruskalMST(int n, List<Edge> edges) {
        List<Edge> mst = new ArrayList<>();
        Collections.sort(edges, (e1, e2) -> e1.weight - e2.weight);
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }

        for (Edge edge : edges) {
            int fromRoot = findRoot(parent, edge.from);
            int toRoot = findRoot(parent, edge.to);
            if (fromRoot != toRoot) {
                mst.add(edge);
                parent[fromRoot] = toRoot;
            }
        }

        return mst;
    }

    static int findRoot(int[] parent, int x) {
        while (parent[x] != x) {
            x = parent[x];
        }
        return x;
    }

    static class Edge {
        int from;
        int to;
        int weight;

        public Edge(int from, int to, int weight) {
            this.from = from;
            this.to = to;
            this.weight = weight;
        }
    }

    public static void main(String[] args) {
        int n = 5;
        List<Edge> edges = new ArrayList<>();
        edges.add(new Edge(0, 1, 2));
        edges.add(new Edge(0, 3, 6));
        edges.add(new Edge(1, 2, 3));
        edges.add(new Edge(1, 3, 8));
        edges.add(new Edge(1, 4, 5));
        edges.add(new Edge(2, 4, 7));
        edges.add(new Edge(3, 4, 9));

        List<Edge> mst = kruskalMST(n, edges);
        System.out.println("Kruskal's 最小生成树:");
        for (Edge edge : mst) {
            System.out.println(edge.from + " - " + edge.to + " 权重:" + edge.weight);
        }
    }
}

此算法的Find函数由边数e决定,时间复杂度为O ( l o g e ) O(loge)O(loge),而外面有一个for循环

e次。所以克鲁斯卡尔算法的时间复杂度为O ( e l o g e ) O(eloge)O(eloge)。

对比两个算法,克鲁斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有

很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。

4. 总结

  • DFS:深度优先遍历算法,我们在进行算法运算时,优先将该路径的当前路径执行完毕,执行完毕或失败后向上回溯尝试其他途径
  • BFS:广度优先遍历算法,我们在进行算法运算时,优先将当前路径点的所有情况罗列出来,然后根据罗列出来的情况罗列下一层
  • DFS和BFS的算法依据:两者均以树的形式进行展开,可以采用树的模型来进行DFS和BFS演示

七、最短路径

在网图和非网图中,最短路径的含义是不同的。由于非网图它没有边上的权值,所谓的最短路径,

其实就是指两顶点之间经过的边数最少的路径;而对于网图来说,最短路径,是指两顶点之间经过

的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。

1. 迪杰斯特拉( Dijkstra )算法

Dijkstra算法用于构建单源点的最短路径---,即图中某个点到任何其他点的距离都是最短的。例

如,构建地图应用时查找自己的坐标离某个地标的最短距离。可以用于有向图,但是不能存在负权

值。

我们以上图为例,通俗点说,这个迪杰斯特拉(Dijkstra) 算法,它并不是一下子求出了v0到v8的最

短路径,而是一步步求出它们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础

上,求得更远顶点的最短路径,最终得到你要的结果。

Dijkstra算法设置一个集合S记录已求得的最短路径的顶点。

在构造的过程中还设置了个辅助数组:

dist[]:记录从源点v0到其他各顶点当前的最短路径长度,它的初态为:若从v0到vi;有弧,则dist[i]

为弧上的权值;否则置dist[i]为∞ ∞∞。

例如,对图6.17中的图应用 Dijkstra算法求从顶点1出发至其余顶点的最短路径的过程,如表6.1所

示。

算法执行过程的说明如下。

  • 初始化:集合S初始为v1 ,v1可达v2和v5,v1不可达v3和v4,因此dist[]数组各元素的初值依次设置为dist[2]=10,dist[3]=∞ ∞∞,dist[4]=∞ ∞∞,dist[5]=5。
  • 第一轮:选出最小值dist[5],将顶点v5并入集合S,即此时已找到v1到v5的最短路径。当v5加入S 后,从v1到集合S中可达顶点的最短路径长度可能会产生变化。因此需要更新dist[]数组。v5可达v2,因v1 → v5 → v2 的距离8比dist[2]=10小,更新dist[2]=8;v5可达v3,v 1 → v 5 → v 3 的距离14,更新dist[3]=14;v5 可达v4,v 1 → v 5 → v 4 的距离7,更新dist[4]=7。
  • 第二轮:选出最小值dist[4],将顶点v4并入集合S。继续更新dist[]数组。v4不可达v2,dist[2]不变;v4可达v3,v 1 → v 5 → v 4 → v 3 的距离13比dist[3]小,故更新dist[3]=13。
  • 笫三轮:选出最小值dist[2],将顶点v2并入集合S。继续更新dist[]数组。v2可达v3,v 1 → v 5 → v 2 → v 3 的距离9比dist[3]小,更新dist[3]=9。
  • 第四轮:选出唯一最小值dist[3],将顶点v3并入集合S,此时全部顶点都已包含在S中。

显然,Dijkstra 算法也是基于贪心策略的。使用邻接矩阵或者带权的邻接表表示时,时间复杂度为O ( V2) 。

人们可能只希望找到从源点到某个特定顶点的最短路径,但这个问题和求解源点到其他所有顶点的最短路径一样

复杂,时间复杂度也为O ( V2) 。

public class Dijkstra {
    public static final int N = 65535;
 
    public static void main(String[] args) {
        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
        //邻接矩阵
        int[][] matrix = new int[vertex.length][vertex.length];
        matrix[0] = new int[]{N, 5, 7, N, N, N, 2};
        matrix[1] = new int[]{5, N, N, 9, N, N, 3};
        matrix[2] = new int[]{7, N, N, N, 8, N, N};
        matrix[3] = new int[]{N, 9, N, N, N, 4, N};
        matrix[4] = new int[]{N, N, 8, N, N, 5, 4};
        matrix[5] = new int[]{N, N, N, 4, 5, N, 6};
        matrix[6] = new int[]{2, 3, N, N, 4, 6, N};
        //创建Graph对象
        Graph graph = new Graph(vertex, matrix);
        graph.showGraph();
        graph.dijkstra(6);
 
 
    }
}
 
class Graph {
    char[] vertex;  //保存结点的数据
    int[][] matrix; //存放边,就是邻接矩阵
    VisitedVertex visitedVertex; //已经访问的顶点的集合
 
    public Graph(char[] vertex, int[][] matrix) {
        this.vertex = vertex;
        this.matrix = matrix;
    }
 
    //显示邻接矩阵
    public void showGraph() {
        for (int i = 0; i < vertex.length; i++) {
            for (int j = 1; j < vertex.length; j++) {
                System.out.printf("%-8d", matrix[i][j]);
            }
            System.out.println();
        }
    }
 
    //迪杰斯特拉算法
    //index 表示出发顶点对应的下标
    public void dijkstra(int index) {
        visitedVertex = new VisitedVertex(vertex.length, index);
        update(index);
        for(int i=0;i<vertex.length;i++){
            index= visitedVertex.getNextIndex(); //访问并返回新的访问节点
            update(index);
        }
        visitedVertex.show();
 
 
 
    }
 
    //更新index下标顶点到周围顶点的前驱顶点,
    public void update(int index) {
        //根据遍历邻接矩阵的matrix[index]行
        int len;
        for (int i = 0; i < matrix[index].length; i++) {
            if(matrix[index][i]==Dijkstra.N)
                continue;
            len = visitedVertex.getDistance(index) + matrix[index][i];
            if (!visitedVertex.isVisted(i) && len < visitedVertex.getDistance(i)) {
                visitedVertex.updateDistance(i, len);  //更新出发顶点到i顶点的距离
                visitedVertex.updatePre(i, index);     //更新i顶点的前驱为index
 
            }
        }
    }
}
 
//已访问项点集合
class VisitedVertex {
    //记录各个顶点是否访问过 true表示访问过, false未访问,会动态更新
    public boolean[] isVisted;
    //每个下标对应的值为前一个顶点下标,会动态更新
    public int[] pre_visited;
    //记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其它顶点的距离,会动态更新,求的最短距离就会存放到dis
    public int[] distance;
 
    //构造器
 
    /**
     * @param length 顶点的个数
     * @param index  出发顶点对应的下标
     */
    public VisitedVertex(int length, int index) {
        this.isVisted = new boolean[length];
        this.pre_visited = new int[length];
        this.distance = new int[length];
        this.isVisted[index] = true;
        //初始化dis数组
        Arrays.fill(distance, Dijkstra.N);
        this.distance[index] = 0;   //出发顶点的访问距离为0
 
    }
 
    //判断下标为index顶点是否访问过
    public boolean isVisted(int index) {
        return isVisted[index];
    }
 
    //更新出发顶点到index顶点的距离
    public void updateDistance(int index, int length) {
        distance[index] = length;
 
    }
    //更新pre这个顶点为index的前驱顶点
 
    /**
     * @param pre   当前节点的前驱节点
     * @param index 当前结点
     */
    public void updatePre(int index, int pre) {
        pre_visited[index] = pre;
 
    }
 
    //返回出发顶点到index顶点的距离
    public int getDistance(int index) {
        return distance[index];
    }
 
    //继续选择并选择新的访问顶点
    public int getNextIndex() {
        int min = Dijkstra.N, index = 0;
        for (int i = 0; i < isVisted.length; i++) {
            if (!isVisted[i] && distance[i] < min) {
                min = distance[i];
                index = i;
 
            }
 
 
        }
        //更新index为已经访问过的
        isVisted[index] = true;
        return index;
 
    }
    public void show(){
        //输出isVisited数组
        System.out.println(Arrays.toString(isVisted));
        //输出pre_visited数组
        System.out.println(Arrays.toString(pre_visited));
        //输出distance数组
        System.out.println(Arrays.toString(distance));
    }
}

2. 弗洛伊德( Floyd )算法

定义一个n阶方阵序列A (−1) , A(0), . . . , A (n−1) ,其中,

式中,A(0) [i] [j] 是从顶点vi到vj、中间顶点的序号不大于k的最短路径的长度。Floyd算法是一个迭

代的过程,每迭代一次,在从vi到vj、的最短路径上就多考虑了一个顶点;经过n次迭代后,所得到

的A (n^−1) [i] [j] 就是vi 到v~j ~的最短路径长度,即方阵A (n−1)中就保存了任意一对顶点之间的最

短路径长度。

上图所示为带权有向图G及其邻接矩阵。算法执行过程的说明如下。

  • 初始化:方阵A (−1) [i] [j] = arcs [i] [j] 。
  • 第一轮:将v0作为中间顶点,对于所有顶点{ i , j } ,如果有A −1 [i] [j] > A−1 [i] [0] + A−1 [0] [j] ,则将A −1 [i] [j] 更新为A −1 [i] [0] + A −1 [0] [j] 。有A −1 [2] [1] > A −1[ 2 ] [ 0 ] + A − 1 [ 0 ] [ 1 ] = 11 ,更新A −1 [2] [1] = 11 ,更新后的方阵标记为A 0 。
  • 第二轮:将v 1作为中间顶点,继续监测全部顶点对{ i , j } 。有A 0 [ 0] [2] > A0 [0] [1] + A0 [1] [ 2 ] = 10 ,更新后的方阵标记为A 1 。
  • 第三轮:将v2 作为中间顶点,继续监测全部顶点对{ i , j } 。有A1 [1] [ 0 ] > A1 [1] [2] + A1 [2] [0 ] = 9 ,更新后的方阵标记为A2。此时A2中保存的就是任意顶点对的最短路径长度。
  • 应用Floyd算法求所有顶点之间的最短路径长度的过程如下表所示。

从这个表中,可以发下一些规律:

可以看出,矩阵中,每一步中红线划掉的部分都不用考虑计算,只需要计算红线外的部分,节省了

计算量。

Floyd算法的时间复杂度为O (V3) 。不过由于其代码很紧凑,且并不包含 其他复杂的数据结构,因

此隐含的常数系数是很小的,即使对于中等规模的输入来说,它仍然是相当有效的。

Floyd算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路。Floyd 算法同样

适用于带权无向图,因为带权无向图可视为权值相同往返二重边的有向图。

也可以用单源最短路径算法来解决每对顶点之间的最短路径问题。轮流将每个顶点作为源点,并且

在所有边权值均非负时,运行一次 Dijkstra算法,其时间复杂度为O ( V3 ) ∗ V = O ( V 3 )。

public class FloydWarshall {
    public static void main(String[] args) {
        int INF = Integer.MAX_VALUE; // 无限大值表示不可达路径
        
        int n = 5; // 图中节点数量
        int[][] graph = new int[n][n]; // 存储图的邻接矩阵
        
        // 初始化图的邻接矩阵(这里只展示部分边)
        for (int i = 0; i < n; i++) {
            Arrays.fill(graph[i], INF);
        }
        graph[0][1] = 2;
        graph[0][3] = 4;
        graph[1][2] = 1;
        graph[1][4] = 6;
        graph[2][3] = 8;
        graph[2][4] = 7;
        graph[3][4] = 9;
        
        floydWarshall(graph);
        
        System.out.println("最小距离矩阵为:");
        printMatrix(graph);
    }
    
    private static void floydWarshall(int[][] graph) {
        int n = graph.length;
        
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                if (graph[i][k] != Integer.MAX_VALUE && graph[k][j] != Integer.MAX_VALUE) {
                    graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j]);
                }
            }
        }
    }
    
    private static void printMatrix(int[][] matrix) {
        for (int[] row : matrix) {
            for (int num : row) {
                System.out.print(num + " ");
            }
            System.out.println();
        }
    }
}

八、拓扑排序

1. 定义

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶

点表示活动的网,我们称为AOV网( Activity On VertexNetwork)。

若用DAG图(有向无环图)表示一个工程,其顶点表示活动,用有向边< V i , V j > 表示活动Vi必

须先于活动Vj进行的这样一种关系。在AOV网中,活动Vi 是活动Vj的直接前驱,活动Vj 是活动Vi的

直接后继,这种前驱和后继关系具有传递性,且任何活动Vi不能以它自己作为自己的前驱或后继。

设G = ( V , E ) 是一个具有n个顶点的有向图,V中的顶点序列V1 , V2 , . . . Vn ,满足若从顶点Vi到

Vj有一条路径,则在顶点序列中顶点Vi必在顶点Vj之前。则我们称这样的顶点序列为一个拓扑序

列。

所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程。每个AOV网都有一个或多个拓扑排序

序列。

2. 算法

对一个AOV网进行拓扑排序的算法有很多,下面介绍比较常用的一种方法的步骤:

  • ① 从AOV网中选择一个没有前驱的顶点并输出。
  • ② 从网中删除该顶点和所有以它为起点的有向边。
  • ③ 重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。如果输出顶点数少了,哪怕是少了一个,也说明这个网存在环(回路),不是AOV网。

上图所示为拓扑排序过程的示例。每一轮选择一个入度为0的顶点并输出,然后删除该顶点和所有

以它为起点的有向边,最后得到拓扑排序的结果为{ 1 , 2 , 4 , 3 , 5 } 。

拓扑排序算法的实现如下:

import java.util.*;

public class TopologicalSort {
    public static List<Integer> topologicalSort(int numCourses, int[][] prerequisites) {
        // 初始化入度表和邻接表
        int[] inDegree = new int[numCourses];
        List<List<Integer>> adjacency = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            adjacency.add(new ArrayList<>());
        }
        for (int[] prerequisite : prerequisites) {
            inDegree[prerequisite[0]]++;
            adjacency.get(prerequisite[1]).add(prerequisite[0]);
        }
        
        // 构建入度为0的节点队列
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        
        // 遍历队列,删除该节点的出边,更新入度表
        List<Integer> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            int curr = queue.poll();
            result.add(curr);
            for (int next : adjacency.get(curr)) {
                inDegree[next]--;
                if (inDegree[next] == 0) {
                    queue.offer(next);
                }
            }
        }
        
        // 判断是否有环,若有环则无法进行拓扑排序
        if (result.size() != numCourses) {
            return new ArrayList<>();
        }
        
        return result;
    }
}

由于输出每个顶点的同时还要删除以它为起点的边,故拓扑排序的时间复杂度为O ( V + E ) 。

此外,利用深度优先遍历也可实现拓扑排序。

用拓扑排序算法处理AOV网时,应注意以下问题:

① 入度为零的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代

表的活动开始或继续。

② 若一个顶点有多个直接后继,则拓扑排序的结果通常不唯一;但若各个顶点已经排在一个线性

有序的序列中,每个顶点有唯一的前驱后继关系,则拓扑排序的结果是唯一的。

③ 由于AOV网中各顶点的地位平等,每个顶点编号是人为的,因此可以按拓扑排序的结果重新编

号,生成AOV网的新的邻接存储矩阵,这种邻接矩阵可以是三角矩阵;但对于一般的图来说,若其

邻接矩阵是三角矩阵,则存在拓扑序列;反之则不一定成立。

九、关键路径

1. 定义

拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短

时间问题。

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如

完成活动所需的

时间),称之为用边表示活动的网络,简称AOE网。AOE网和AOV网都是有向无环图,不同之处在

于它们的边和顶点所代表的含义是不同的,AOE网中的边有权值;而AOV网中的边无权值,仅表

示顶点之间的前后关系。

AOE网具有以下两个性质:

  • ① 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
  • ② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。

如上图的AOE网,在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的

开始;网中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。我们把

路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,

在关键路径上的活动叫关键活动。

完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关

键活动影响了整个工程的时间,即若关键活动不能按时完成,则整个工程的完成时间就会延长。因

此,只要找到了关键活动,就找到了关键路径,也就可以得出最短完成时间。

2. 算法

在分析算法之前,需要了解几个重要的参数:

  1. 事件的最早发生时间ve:即顶点Vk 的最早发生时期。
  2. 事件的最晚发生时间vl:即顶点Vk 的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个工期。
  3. 活动的最早开始时间e:即弧ai 的最早发生时间。
  4. 活动的最晚开始时间l:即弧ai 的最晚发生时间,也就是不推迟工期的最晚开工时间。
  5. 一个活动ai 的最迟开始时间 (i) 和其最早开始时间e(i)的差额d(i) = l (i) − e (i) :它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动ai 可以拖延的时间。若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称l(i) − e (i) = 0 即l(i) = e(i) 的活动ai 是关键活动。

求关键路径的算法步骤如下:

  1. 从源点出发,令ve (源点) = 0, 按拓扑排序求其余顶点的最早发生时间ve( )。
  2. 从汇点出发,令vl( 汇点 ) = ve ( 汇点 ),按逆拓扑排序求其余顶点的最迟发生时间vl( )。
  3. 根据各顶点的ve()值求所有弧的最早开始时间e()。
  4. 根据各顶点的vl()值求所有弧的最迟开始时间l()。
  5. 求AOE网中所有活动的差额d(), 找出所有d()=0的活动构成关键路径。

上图所示为求解关键路径的过程,简单说明如下:

  1. 求ve():初始ve(1)=0,在拓扑排序输出顶点的过程中,求得ve(2)=3ve(2)=3,ve(3)=2,ve(4)=max{ve(2)+2,ve(3)+4}=max{5,6}=6 ,ve(6)=max{ve(5)+1,ve(4)+0,ve(3)+3}=max{7,8,5}=8。
  2. 求vl():初始vl(6)=8,在逆拓扑排序出栈过程之中,求得vl(5)=7,vl(4)=6,vl(3)=min{vl(4)−4,vl(6)−3}=min{2,5}=2,vl(2)=min{vl(5)−3,vl(4)−2}=min{4,4}=4,vl(1)必然为0而无需再求。
  3. 弧的最早开始时间e()等于该弧的起点的顶点的ve(),求得结果如上表所示。
  4. 弧的最迟开始时间l(i)等于该弧的终点的顶点的vl()减去该弧持续的时间,求得结果如上表所示。
  5. 根据l(i)-e(i)=0l(i)−e(i)=0的关键活动,得到的关键路径为( v1 , v3 , v4 , v6 ) 。

对于关键路径,需要注意以下几点:

① 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活

动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动

就可能会变成非关键活动。

② 网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动速

度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期

的目的。

/// <summary>
/// 关键路径算法
/// </summary>
/// <param name="graph"></param>
private void CriticalPath<T>(GraphByAdjacencyList<T> graph)
{
	// 通过拓扑排序计算事件最早发生时间
	var topoStack = Topological2(graph, out int[] etv);
	// 定义事件最晚发生时间并初始化为终点的最早发生时间
	int[] ltv = new int[graph.Count];
	for (int i = 0; i < graph.Count; i++)
	{
		ltv[i] = etv[graph.Count - 1];
	}
	// 求事件最晚发生时间
	while (topoStack.Count > 0)
	{
		int nodeIndex = topoStack.Pop();
		// 遍历邻接链表
		var edge = graph.Nodes[nodeIndex].next;
		while (edge != null)
		{
			// 如果(下一个事件的最晚发生时间 - 活动时间) < 当前记录的最晚发生时间
			// 则意味着需要把工期提前
			if (ltv[edge.index] - edge.weight < ltv[nodeIndex] )
			{
				ltv[nodeIndex] = ltv[edge.index] - edge.weight;
			}
			edge = edge.next;
		}
	}

	for (int i = 0; i < graph.Count; i++)
	{
		// 遍历所有边
		var edge = graph.Nodes[i].next;
		while (edge != null)
		{
			// 最早开工时间 = 起始事件的最早发生时间
			int ete = etv[i];
			// 最晚开工时间 = 结束事件的最晚发生时间 - 活动时间
			int lte = ltv[edge.index] - edge.weight;
			// 最早开工时间 == 最晚开工时间,说明是关键活动
			if (ete == lte)
			{
				// 打印路径
				Console.Write($" {graph.Nodes[i].data}->{graph.Nodes[edge.index].data} ");
			}
			edge = edge.next;
		}
	}
}

十、知识小结

图是计算机科学中非常常用的一类数据结构,同时也是最复杂的数据结构了,对它的学习,涉及到

顺序表、链表、栈、队列、树等之前学的几乎所有数据结构,所以学习图之前要对这几种数据结构

都要有所了解才行。

相关推荐
Camellia-Echo8 分钟前
[LeetCode]全排列I,II
数据结构·算法·leetcode
gentle_ice14 分钟前
leetcode——组合总和(回溯算法详细讲解)
数据结构·算法·leetcode
因兹菜20 分钟前
【LeetCode】day15 142.环形链表II
算法·leetcode·链表
大大大大小小27 分钟前
问卷调查系统Two-Step-Kmeans-前端后端搭建完成
前端·算法·kmeans
青椒大仙KI1128 分钟前
25/2/6 <机器人基础> 运动学中各连杆的变换矩阵求法
算法·矩阵·机器人
且撷相思红豆枝1 小时前
ESP32开发学习记录---》GPIO
开发语言·c++·算法
LMQ62 小时前
[数据结构] 线性表和顺序表
数据结构
kim_puppy3 小时前
树的基本概念,并查集复习(学习记录)
c语言·数据结构·学习·算法
查理零世9 小时前
【算法】动态规划专题④ ——LCS(最长公共子序列)+ LPS(最长回文子序列) python
python·算法·动态规划