相关笔记

概览

本节介绍图的两种标准计算机表示方法:邻接表(adjacency-list)和邻接矩阵(adjacency-matrix)。邻接表将每个顶点 的邻居存储在链表 中,空间开销为 ,适合稀疏图;邻接矩阵用一个 的矩阵 表示, 指示边 是否存在,空间开销为 ,适合稠密图。两种表示各有优劣,选择取决于图的稀疏程度和需要支持的图操作。

核心要点:

  • 顶点集 边集 组成,分为有向图和无向图
  • 邻接表:空间 ,适合稀疏图,枚举邻居高效但判断边存在性较慢
  • 邻接矩阵:空间 ,适合稠密图,判断边存在性 但枚举邻居较慢
  • 自环(self-loop)和多重边(parallel edge)是图的特殊情形
  • 工程中大规模图计算还使用 CSR 等压缩格式

知识结构总览

flowchart TD
    A["20.1 图的表示"] --> B["图的基本概念"]
    A --> C["邻接表表示"]
    A --> D["邻接矩阵表示"]
    A --> E["两种表示的对比"]
    A --> F["习题拓展"]

    B --> B1["有向图 vs 无向图"]
    B --> B2["自环"]
    B --> B3["多重边"]
    B --> B4["稀疏图 vs 稠密图"]

    C --> C1["Adj[u] 链表"]
    C --> C2["空间: O(V+E)"]
    C --> C3["适合稀疏图"]
    C --> C4["枚举邻居 O(degree)"]

    D --> D1["矩阵 A[i][j]"]
    D --> D2["空间: O(V²)"]
    D --> D3["适合稠密图"]
    D --> D4["边查询 O(1)"]

    E --> E1["空间对比"]
    E --> E2["操作效率对比"]
    E --> E3["适用场景"]

    F --> F1["转置图"]
    F --> F2["图的平方"]
    F --> F3["通用汇点"]
    F --> F4["关联矩阵"]
    F --> F5["散列邻接表"]

    style A fill:#4a90d9,color:#fff
    style C fill:#27ae60,color:#fff
    style D fill:#e67e22,color:#fff

核心思想

核心思想

图的表示本质上是在存储效率操作效率之间做权衡。邻接表只存储实际存在的边,空间紧凑,但判断某条边是否存在需要遍历链表;邻接矩阵为每一对可能的顶点都预留了存储位置,判断边存在性只需 ,但空间浪费在大量不存在的边上。选择哪种表示取决于图的稀疏程度和算法需要频繁执行的操作类型。

图的基本概念

图(Graph)

一个 顶点集(vertex set)边集(edge set) 组成。每条边是一个顶点对 ,其中

  • 有向图(directed graph)中,边 是有序对, 称为(tail), 称为(head),边从 指向
  • 无向图(undirected graph)中,边 是无序对, 表示同一条边
  • 如果 ,则称为自环(self-loop)
  • 如果多条边连接同一对顶点,则称为多重边(parallel edges)或平行边
  • 简单图(simple graph)是没有自环和多重边的图

稀疏图与稠密图

  • 稀疏图,即边的数量远小于顶点数的平方。典型例子:社交网络(Facebook 好友图中
  • 稠密图 接近 。典型例子:全连接神经网络中神经元之间的连接
  • 分界线通常取 作为经验阈值

邻接表表示

邻接表(Adjacency List)

邻接表表示由一个包含 条链表的数组 组成。对于每个顶点 ,邻接表 包含所有满足 的顶点

  • 对于有向图,边 出现在
  • 对于无向图,边 同时出现在
  • 空间复杂度(有向图)或 (无向图)

邻接表的伪代码描述:

// 邻接表的数据结构(概念描述)
for each vertex u ∈ G.V
    Adj[u] = 空链表
for each edge (u, v) ∈ G.E
    将 v 加入 Adj[u]    // 有向图
    // 无向图还需将 u 加入 Adj[v]

邻接表上的基本操作复杂度:

操作复杂度说明
枚举顶点 的所有邻居遍历
判断边 是否存在 中查找
添加边 链表头部插入
删除边 需要在链表中定位
计算所有顶点的度数之和遍历所有邻接表

邻接矩阵表示

邻接矩阵(Adjacency Matrix)

邻接矩阵表示使用一个 的矩阵 ,其中:

  • 对于带权图 存储边 的权值,不存在边时存储特殊值(如
  • 空间复杂度

邻接矩阵上的基本操作复杂度:

操作复杂度说明
判断边 是否存在直接访问
枚举顶点 的所有邻居扫描
添加边 设置
删除边 设置
计算顶点 的度数统计 中 1 的个数

两种表示的对比

邻接表 vs 邻接矩阵

比较维度邻接表邻接矩阵
空间
判断边 是否存在
枚举所有边
枚举 的邻居
添加边
删除边
适合场景稀疏图稠密图
自环/多重边自然支持需特殊处理
加权图链表节点存权值矩阵存权值

补充理解与拓展

邻接表 vs 邻接矩阵的工程选型

在实际工程中,选择图的表示方法需要综合考虑图的规模、稀疏程度和操作类型:

  • 社交网络(稀疏图):Facebook 好友图中,平均每人约有 200-500 个好友,而用户数以十亿计。 是天文数字,必须使用邻接表。Twitter 的关注关系图同样如此。
  • 道路网络(稀疏图):城市道路网中,每个路口平均连接 3-4 条道路,,使用邻接表。
  • 全连接神经网络(稠密图):每层中每个神经元与下一层所有神经元相连,,使用邻接矩阵(即权重矩阵)。
  • 网页链接图(稀疏图):万维网中每个页面平均链接到约 50-100 个其他页面,使用邻接表。
  • 蛋白质相互作用网络(稀疏图):每个蛋白质平均与少量其他蛋白质相互作用,使用邻接表。

经验法则:当 时(即平均度数小于 ),邻接表几乎总是更好的选择。

来源:CLRS Chapter 22; Leskovec et al., “Community structure in large networks”, 2009

散列邻接表(习题 22.1-8 的工程意义)

习题 22.1-8 提出将每个邻接表 散列表(hash table)代替链表。这种表示在工程中有重要应用:

  • 边查询:判断边 是否存在的时间从 降至 期望时间
  • 添加/删除边:均为 期望时间
  • 空间:仍为 ,与邻接表相同
  • 缺点:无法在 时间内枚举所有邻居(散列表遍历需要扫描所有桶),不支持有序枚举邻居
  • 最坏情况:所有边散列冲突时退化为

适用场景:需要频繁查询边是否存在,但不需要频繁枚举所有邻居的应用。例如:社交网络中快速判断两人是否是好友(“关注”按钮的状态查询)。

实际系统中,Neo4j 图数据库的 adjacency cache 就使用了类似思路;NetworkX(Python 图库)也提供了基于字典的邻接表实现。

CSR 格式——大规模图计算的标准存储格式

在高性能图计算领域,CSR(Compressed Sparse Row)格式是最广泛使用的图存储格式:

CSR 格式由两个数组组成:

  • offsets[0..|V|]offsets[i] 表示顶点 的邻居在 edges 数组中的起始位置
  • edges[0..|E|]:按顶点编号顺序依次存储每个顶点的邻居

CSR 的优势:

  • 空间:,与邻接表相同,但常数因子更小(无指针开销)
  • 缓存友好:所有数据存储在连续内存中,CPU 缓存命中率远高于链表式邻接表
  • 并行友好:天然支持 SIMD 向量化操作

CSR 的劣势:

  • 添加/删除边的代价高(需要移动大量数据),适合静态图
  • 不支持高效的边查询(判断某条边是否存在仍需扫描)

应用:

  • GraphBLAS(图计算标准 API)以 CSR 为基础格式
  • SuiteSparse、cuGraph(NVIDIA GPU 图库)均使用 CSR
  • PageRank、BFS、SSSP 等图算法在大规模图上的高效实现几乎都基于 CSR

来源:GraphBLAS C API Specification; NVIDIA cuSPARSE Documentation; Bell & Dalton, “Sparse Matrix-Vector Multiplication”, 2013


易混淆点与辨析

无向图的邻接表中每条边出现两次

常见错误:认为无向图的邻接表中每条边只存储一次。

正确理解:在无向图的邻接表表示中,边 同时出现在 中。因此无向图的邻接表总长度为 ,空间仍为 。遍历所有边时需要注意去重,否则每条边会被处理两次。

邻接矩阵中无向图是对称矩阵

常见错误:认为无向图的邻接矩阵不一定对称。

正确理解:对于无向图, 是同一条边,因此 ,邻接矩阵是对称矩阵。可以利用这一性质只存储上三角或下三角部分,将空间减半至

自环在邻接矩阵中的表示

常见错误:认为自环 在邻接矩阵中不影响度数计算。

正确理解:自环 使 。在有向图中,自环对顶点 的出度和入度各贡献 1;在无向图中,自环对顶点 的度数贡献 2(因为 中出现两次——一次作为”出边”,一次作为”入边”)。

稠密图上邻接矩阵不一定比邻接表慢

常见错误:认为邻接矩阵总是比邻接表慢。

正确理解:对于稠密图(),邻接矩阵的 空间与邻接表的 空间同级。此时邻接矩阵的 边查询优势凸显,且连续内存布局带来更好的缓存性能。许多图算法(如 Floyd-Warshall)在邻接矩阵上实现更简洁高效。


习题精选

题号题目描述难度
22.1-1给定图 的邻接表表示,给出 时间的算法计算 的转置的邻接表表示★★☆
22.1-2给定邻接表表示,给出 时间的算法计算 的邻接表表示★★★
22.1-3给定邻接矩阵表示,给出 时间的算法判断某个顶点是否是通用汇点★★☆
22.1-4给定邻接矩阵表示,给出 时间的算法判断某个顶点是否是通用汇点(另一种方法)★★☆
22.1-5有向图和无向图的关联矩阵定义,比较两种表示★★☆
22.1-6给定邻接矩阵,给出 时间的算法判断通用汇点是否存在★★★
22.1-7有向图的关联矩阵中每列恰好有两个 1(一个 +1,一个 -1),为什么?★☆☆
22.1-8用散列表代替链表实现邻接表,分析各操作的时间复杂度★★★

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 12Graph Representationhttps://www.youtube.com/watch?v=s33lTmLwDMsErik Demaine 教授,讲解图的邻接表和邻接矩阵表示
Abdul BariGraph Representationhttps://www.youtube.com/watch?v=2xfXyRaqVr4直观对比两种表示方法,含示例
WilliamFisetGraph Theory Basicshttps://www.youtube.com/watch?v=LFKZLXVO-Dg图论基础系列,含表示方法讨论
GeeksforGeeksGraph and its representationshttps://www.geeksforgeeks.org/graph-and-its-representations/文字+代码,含 C/Java/Python 实现

教材原文

原文摘录(CLRS 第4版 22.1节)

A graph is a structure consisting of a set of vertices and a set of edges . Each edge is a pair of vertices from . In a directed graph, each edge is an ordered pair ; in an undirected graph, each edge is an unordered pair .

是一种由顶点集 和边集 组成的结构。每条边是 中的一对顶点。在有向图中,每条边是有序对 ;在无向图中,每条边是无序对

原文摘录(CLRS 第4版 22.1节)

We assume that the input graph is represented using adjacency lists. The adjacency-list representation of a graph consists of an array of lists, one for each vertex in . For each , the adjacency list contains all the vertices such that there is an edge .

我们假设输入图 使用邻接表表示。图 的邻接表表示由一个包含 条链表的数组 组成, 中每个顶点对应一条链表。对于每个 ,邻接表 包含所有满足 的顶点

原文摘录(CLRS 第4版 22.1节)

For a weighted graph, we simply store the weight of the edge along with the vertex in the adjacency list. The adjacency-matrix representation of a graph is a matrix , where if , and otherwise.

对于带权图,我们只需将边的权值与顶点一起存储在邻接表中。图 的邻接矩阵表示是一个 的矩阵 ,其中 ,否则


参见Wiki

  • (概念页尚未创建)

第20章-基本图算法 图的表示