好的,看到您正在学习数据结构中的图论部分,这真是太棒了!图是一个非常重要且应用广泛的数据结构。您截图中的这两个标题 "什么是图 - 定义" 和 "什么是图 - 邻接矩阵表示法" 是学习图论的基石。

我将为您详细拆解这两个知识点,并结合代码实现进行说明,希望能帮助您更好地理解。


第一部分:什么是图 - 定义 (6.1.1)

在我们深入代码之前,必须先建立清晰、扎实的概念。

1. 直观理解:图是什么?

想象一下你的社交网络:你和你的朋友们可以被看作是一个个的“点”,而你们之间的好友关系就是连接这些“点”的“线”。或者想象一张地铁线路图:每个地铁站是一个“点”,而连接两个站点的地铁线路就是一条“线”。

在数据结构中,这种由“点”和“线”组成的结构,就叫做图 (Graph)

  • 点 (Vertex / Node):我们称之为 顶点节点。在社交网络中,顶点就是用户;在地铁图中,顶点就是站点。

  • 线 (Edge):我们称之为 。边连接了两个顶点。在社交网络中,边就是好友关系;在地铁图中,边就是线路。

所以,图的本质就是 “顶点”的集合“边”的集合。我们通常用 G = (V, E) 来表示一个图,其中:

  • G 代表整个图 (Graph)。

  • V 代表所有顶点的集合 (Vertex set)。

  • E 代表所有边的集合 (Edge set)。

2. 图的分类和重要术语

为了更精确地描述图,我们需要了解一些关键的术语和分类。

a. 有向图 (Directed Graph) vs. 无向图 (Undirected Graph)

  • 无向图:边是没有方向的。如果顶点 A 和顶点 B 之间有一条边,那么你可以从 A 到 B,也可以从 B 到 A。这就像微信好友关系,你是我的好友,我也是你的好友,关系是相互的。

  • 有向图:边是有方向的。如果有一条从顶点 A 指向顶点 B 的边(称为),那么你只能从 A 到 B,不能从 B 到 A(除非还有一条从 B 指向 A 的边)。这就像微博的“关注”关系,我关注了你,但你不一定关注我。

b. 带权图 (Weighted Graph) vs. 无权图 (Unweighted Graph)

  • 无权图:所有的边都一样,没有“权重”或“成本”的区别。可以认为每条边的权重都是 1。

  • 带权图:每条边都有一个关联的数值,称为权重 (Weight)成本 (Cost)。这个权重可以代表各种含义,比如距离、时间、费用等。例如,在地图应用中,连接两个城市的边的权重可以是从一个城市到另一个城市的距离或所需时间。

c. 其他重要术语

  • 顶点的度 (Degree)

    • 无向图中,一个顶点的度是指与该顶点相连的边的数量。

    • 有向图中,度的概念分为入度 (In-degree)出度 (Out-degree)

      • 入度:指向该顶点的边的数量。

      • 出度:从该顶点出发的边的数量。

  • 路径 (Path):从一个顶点到另一个顶点经过的顶点序列。

  • 环 (Cycle):一个起点和终点相同的路径。

  • 连通图 (Connected Graph):在无向图中,如果任意两个顶点之间都存在路径,则称该图为连通图。

  • 强连通图 (Strongly Connected Graph):在有向图中,如果任意两个顶点 uv 之间,既存在从 uv 的路径,也存在从 vu 的路径,则称该图为强连通图。


第二部分:邻接矩阵表示法 (6.1.2)

我们已经理解了图的概念,但计算机无法直接“看懂”我们画的图。我们需要一种方法将图的结构用计算机能理解的方式存储起来。邻接矩阵 (Adjacency Matrix) 就是其中最常用的一种方法。

1. 什么是邻接矩阵?

邻接矩阵是一个二维数组(也就是矩阵),用来表示顶点之间的连接关系。

假设一个图有 N 个顶点,我们就创建一个 N x N 的矩阵,我们称之为 adjMatrix。

adjMatrix[i][j] 的值表示从顶点 i 到顶点 j 的关系。

规则如下:

  • 对于无权图

    • 如果从顶点 i到顶点j 存在一条边,则 adjMatrix[i][j] = 1

    • 如果不存在边,则 adjMatrix[i][j] = 0

  • 对于带权图

    • 如果从顶点 i到顶点j 存在一条边,则 adjMatrix[i][j] = weight (边的权重)。

    • 如果不存在边,我们通常用一个特殊值来表示,比如 0 (如果权重都是正数) 或者 无穷大 ()。

2. 邻接矩阵的特点

  • 对称性:对于无向图,其邻接矩阵是对称的。因为如果 ij 之间有边,那么 adjMatrix[i][j]adjMatrix[j][i] 都会被标记。

  • 空间复杂度:邻接矩阵需要 N * N 的空间,即 O(N2),其中 N 是顶点数。这意味即使图很“稀疏”(边的数量远小于顶点数量的平方),它也需要占用同样大的空间。

  • 查询效率:判断两个顶点之间是否有边非常快,只需要访问矩阵中对应的元素 adjMatrix[i][j] 即可,时间复杂度为 O(1)。

  • 遍历邻居:要找到一个顶点的所有邻居(即与它相连的所有顶点),需要遍历矩阵的一整行,时间复杂度为 O(N)。

3. 示例

让我们用一个具体的例子来看看如何构建邻接矩阵。

例1:一个无向无权图

这个图有 5 个顶点 (0, 1, 2, 3, 4)。我们可以创建一个 5x5 的矩阵。

  • 顶点 0 连接了 1 和 4 -> matrix[0][1]=1, matrix[1][0]=1matrix[0][4]=1, matrix[4][0]=1

  • 顶点 1 连接了 0, 2, 3, 4 -> matrix[1][0]=1, matrix[1][2]=1, matrix[1][3]=1, matrix[1][4]=1 (反向亦然)

  • ... 以此类推

最终的邻接矩阵如下:

0 1 2 3 4
0 0 1 0 0 1
1 1 0 1 1 1
2 0 1 0 1 0
3 0 1 1 0 1
4 1 1 0 1 0

注意:对角线 matrix[i][i] 通常为 0,表示顶点到自身的距离为0。整个矩阵沿对角线对称。


第三部分:代码实现与逐行注释

下面我们用 Java 来实现一个使用邻接矩阵表示的图。这个类将能够处理无向带权图。

Java

// 引入 Java 的工具包,用于打印数组等
import java.util.Arrays;

/**
 * Graph 类用于表示一个使用邻接矩阵实现的图。
 */
public class AdjacencyMatrixGraph {

    // 私有成员变量,用于存储顶点的数量
    private int numVertices;
    
    // 私有成员变量,用于存储邻接矩阵
    // 它是一个二维数组,adjMatrix[i][j] 存储从顶点 i 到顶点 j 的边的信息
    private int[][] adjMatrix;

    /**
     * 构造函数,用于初始化一个图对象
     * @param numVertices 图中顶点的数量
     */
    public AdjacencyMatrixGraph(int numVertices) {
        // 将传入的顶点数量赋值给成员变量
        this.numVertices = numVertices;
        // 根据顶点数量,创建一个 N x N 的二维数组作为邻接矩阵
        adjMatrix = new int[numVertices][numVertices];

        // 初始化邻接矩阵
        // 遍历矩阵的每一行
        for (int i = 0; i < numVertices; i++) {
            // 遍历矩阵的每一列
            for (int j = 0; j < numVertices; j++) {
                // 默认情况下,任何两个顶点之间都没有边
                // 我们用 0 来表示没有边。如果是有权图,可以用一个特殊值(如-1或无穷大)
                adjMatrix[i][j] = 0; 
            }
        }
    }

    /**
     * 添加一条边到图中(这里是无向带权图)
     * @param i 边的第一个顶点
     * @param j 边的第二个顶点
     * @param weight 边的权重
     */
    public void addEdge(int i, int j, int weight) {
        // 检查传入的顶点索引是否有效,防止数组越界
        if (i >= 0 && i < numVertices && j >= 0 && j < numVertices) {
            // 在邻接矩阵中设置从 i 到 j 的边的权重
            adjMatrix[i][j] = weight;
            // 因为这是无向图,所以从 j 到 i 的边也存在且权重相同
            adjMatrix[j][i] = weight;
        } else {
            // 如果索引无效,打印错误信息
            System.out.println("无效的顶点索引!");
        }
    }

    /**
     * 打印邻接矩阵,方便我们查看图的结构
     */
    public void printGraph() {
        // 打印一个标题
        System.out.println("图的邻接矩阵表示:");
        // 在第一行打印列的表头(顶点编号)
        System.out.print("  ");
        for (int i = 0; i < numVertices; i++) {
            System.out.print(i + " ");
        }
        System.out.println();
        System.out.println("--".repeat(numVertices + 1));

        // 遍历矩阵的每一行
        for (int i = 0; i < numVertices; i++) {
            // 在行首打印行的表头(顶点编号)
            System.out.print(i + "|");
            // 遍历该行的每一列
            for (int j = 0; j < numVertices; j++) {
                // 打印矩阵中的元素,并加上一个空格
                System.out.print(adjMatrix[i][j] + " ");
            }
            // 打印完一行后换行
            System.out.println();
        }
    }

    /**
     * main 方法,程序的入口,用于测试我们的 Graph 类
     */
    public static void main(String[] args) {
        // 创建一个有 5 个顶点的图实例
        // 顶点将被编号为 0, 1, 2, 3, 4
        AdjacencyMatrixGraph g = new AdjacencyMatrixGraph(5);

        // 添加边和它们的权重
        // 例如,添加一条从顶点 0 到顶点 1 的边,权重为 2
        g.addEdge(0, 1, 2); 
        g.addEdge(0, 4, 8);
        g.addEdge(1, 2, 3);
        g.addEdge(1, 3, 6);
        g.addEdge(1, 4, 5);
        g.addEdge(2, 3, 4);
        g.addEdge(3, 4, 7);
        
        // 调用 printGraph 方法,将图的邻接矩阵打印到控制台
        g.printGraph();
    }
}

运行 main 方法后的输出结果:

图的邻接矩阵表示:
  0 1 2 3 4 
------------
0|0 2 0 0 8 
1|2 0 3 6 5 
2|0 3 0 4 0 
3|0 6 4 0 7 
4|8 5 0 7 0 

这个输出完美地反映了我们在代码中添加的边的结构。例如,adjMatrix[0][1]adjMatrix[1][0] 的值都是 2,代表顶点 0 和 1 之间有一条权重为 2 的边。值为 0 的地方表示没有直接相连的边。


第四部分:扩展说明

1. 邻接矩阵的优缺点总结

优点:

  1. 实现简单:使用二维数组即可,非常直观。

  2. 查询速度快:检查任意两个顶点 ij 之间是否存在边(以及边的权重)的时间复杂度是 O(1),非常高效。

  3. 方便进行矩阵运算:在一些高级的图算法中,如图的幂运算求两点间长度为k的路径数量,矩阵形式非常方便。

缺点:

  1. 空间浪费:空间复杂度固定为 O(N2)。对于稀疏图(Sparse Graph,即边的数量远小于 N2 的图),邻接矩阵会浪费大量存储空间来表示不存在的边(值为0或)。例如,一个有10000个顶点但只有20000条边的图,邻接矩阵需要存储 10000*10000 = 1亿 个整数,而其中绝大部分都是0。

  2. 遍历邻居效率低:要找到顶点 v 的所有邻居,需要遍历矩阵的第 v 行,时间复杂度为 O(N),即使 v 只有一个邻居,也需要检查 N-1 个不存在的连接。

2. 何时使用邻接矩阵?

基于以上优缺点,我们通常在以下情况选择使用邻接矩阵:

  • 当图是稠密图(Dense Graph)时,即边的数量接近 N2。这时,空间的浪费问题不严重。

  • 当需要频繁、快速地查询任意两点之间是否存在边时。

3. 另一种重要表示法:邻接表 (Adjacency List)

为了克服邻接矩阵在稀疏图上的缺点,我们有另一种非常重要的数据结构:邻接表

  • 结构:邻接表由一个包含所有顶点的数组(或哈希表)构成。数组中的每个元素对应一个顶点,并链接到一个链表(或动态数组),该链表存储了所有与这个顶点相邻的顶点。

  • 空间复杂度:O(N+E),其中 N 是顶点数,E 是边数。对于稀疏图,这比 O(N2) 高效得多。

  • 优点:节省空间,并且遍历一个顶点的所有邻居非常高效。

  • 缺点:查询两个顶点之间是否存在边需要遍历其中一个顶点的邻接链表,时间复杂度最坏为 O(N),不如邻接矩阵的 O(1) 快。

邻接表示例:

对于我们之前那个无向无权图:

  • Vertex 0 -> [1, 4]

  • Vertex 1 -> [0, 2, 3, 4]

  • Vertex 2 -> [1, 3]

  • Vertex 3 -> [1, 2, 4]

  • Vertex 4 -> [0, 1, 3]

在后续的学习中,您一定会遇到邻接表,它是实现很多如图的遍历(DFS, BFS)、最短路径(Dijkstra)等算法时更常用的表示方法。

希望这份详细的讲解能帮助您扎实地掌握图的基础知识和邻接矩阵表示法。祝您学习顺利!如果您有任何其他问题,随时可以提问。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐