返回上一级
4229 字
21 分钟
图

p38(16.1 分钟)、p39(14.7 分钟)、p40(13.2 分钟)、p41(14.2 分钟)、p42(26.8 分钟)

图是什么#

与线性结构、树的对比#

数组、链表、栈、队列 → 线性数据结构
树 → 非线性数据结构(层次结构)
图 → 另一种非线性数据结构

对照着看树的特殊性:

N 个节点的树必定恰好有 N−1 条边
从根出发必须能到达所有节点,且根到某节点只有唯一一条路径

图则没有支配节点间连接的规则,边可以任意方式连接——树只是图的一种特殊类型。

形式定义#

图 G = (V, E)
V 是顶点集,E 是边集
这是一个「有序对」(第一个对象必须是顶点集,第二个必须是边集)
有序对 (a, b):顺序重要,(a,b) ≠ (b,a)(除非 a = b),写成圆括号
无序对 {a, b}:顺序不重要,{a,b} = {b,a},写成花括号

边的两种类型#

有向边无向边
连接单向双向
表示有序对 (u, v)无序对 {u, v}
图形带箭头的边普通边
要表示 v→u必须另画一条边 (v, u)同一条边即包含

有向图:所有边均为有向边;无向图:所有边均为无向边。

一张图同时含两种边是可能的,但这里不研究这类图。

无向图可以重绘为有向图(每条无向边 → 两条方向相反的有向边);有向图不一定能重绘为无向图。

建模应用(四个例子)#

场景图类型为什么
社交网络(Facebook)无向、未加权友谊是相互的
万维网/互联网有向、未加权「页面 A 链到 B 不代表 B 链回 A」
城际公路网无向、加权假设高速双向通行,长度不同
市内道路网有向、加权城市里有单行道

推荐好友问题#

给拉玛推荐「朋友的朋友」:拉玛有 3 个朋友(埃拉、鲍勃、凯蒂),鲍勃有 3 个朋友(汤姆、萨姆、里)、凯蒂有 2 个朋友(里、斯瓦蒂)→ 共可推荐 4 位用户(里被算了两次)。

用图论术语表述:找出所有「到给定节点的最短路径长度等于 2」的节点。

网络爬虫本质上就是图遍历。

加权图#

有些连接不能同等看待(道路长度不同),给每条边关联权重/代价。

例:城市 A 到 D 选最佳路线。

不计权重:经 B、C 的路线与经 E、F 的路线都是 3 条边,「同样好」
经 E 的黄色路线只有 2 条边,「最好」
计入权重:需要把路径上各边权重相加算总成本
→ 最短(最优)路线变成经 B 和 C 的那条

可以把所有图都视为加权图;未加权图可看作所有边权重相同的加权图,通常假定权重为 1。

基本属性#

记号#

|V| 表示顶点数量
|E| 表示边的数量
(集合的基数,用与绝对值相同的记号)

特殊类型的边#

自环(self-loop) 一条边只涉及一个顶点、两个端点相同
例:网页指向自己的链接(点击页眉链接只是刷新)
多重边(multi-edge) 同一条边在图中出现不止一次
例:城市间航班网络(一对城市间有多趟航班)

自环和多重边会让处理图的工作变复杂。

简单图(simple graph):不含自环也不含多重边。这里大多研究简单图。

边数的取值范围#

最少边数 = 0(可以不画任何边,节点可以完全不相连)。

简单图的最大边数:

最大边数取值范围
有向图N × (N−1)[0, N(N−1)]
无向图N × (N−1) / 2[0, N(N−1)/2]

无向图是一半的原因:无向图中一对节点之间只能有一条双向边,不能有方向不同的两条边。

具体数值:

有向图顶点数 = 10 → 最大 90 条边
有向图顶点数 = 100 → 最大 9900 条边
→ 最大边数近似于顶点数量的平方

密集图 vs 稀疏图#

定义存储
密集图(dense)边数接近可能的最大边数,约为顶点数量的平方量级通常用邻接矩阵
稀疏图(sparse)边数确实很少,通常接近于顶点数量、且不超过这个数量通常用邻接表

两者没有明确界限,完全取决于具体情境;但这是重要分类——很多决策基于图是密集还是稀疏。

路径相关术语#

这一块最容易写漏。

路径(path) 一系列顶点序列,其中每一对相邻顶点之间都有边相连
简单路径(simple path) 没有重复顶点(顶点不重复则边也不会重复)
漫步(walk) 允许重复顶点/边
轨迹(trail) 顶点可以重复但边不能重复的漫步

图论中「路径」的用法不统一。大多数时候说「路径」就指简单路径;如果允许重复,用「漫步」这个术语。

所以:路径 = 顶点或边均不重复的漫步。

一个关键断言:两个顶点之间如果存在一条有重复的漫步,那么必然也存在一条简单路径。

因此寻找简单路径最有意义。这里的约定是:说「路径」即指简单路径;若不是简单路径会明确说明。

连通性#

术语适用定义
连通无向图任意一个顶点到其他任意顶点都存在路径
强连通有向图任意一个顶点到其他任意顶点都存在路径
弱连通有向图不是强连通,但把所有边视作无向边后变成连通图

无向图就说「连通」,有向图就说「强连通」,记住这两个就够了。

反例:某图中能从 A 到 C,但不能从 C 到 A → 非强连通。

应用:市内道路网应当始终是强连通的。

环#

闭合行走(closed walk):起点和终点是同一个顶点,且长度必须大于 0
长度:路径中边的数量
简单环(simple cycle):闭合行走,且除起点/终点顶点外没有其他顶点或边被重复
无环图(acyclic):不含环的图

通常说「环」指简单环。

树 = 无向无环连通图的实例:

树中可以有闭合行走(边的重复),但不存在简单环
此外树还必须连通(无向无环图不只有树这一种)

有向无环图 = DAG:不存在「始于且终于同一个顶点、长度大于 0」的路径。

图中的环会给「从一个顶点到另一个顶点找最短路线」这类算法设计带来诸多问题。

三种表示法#

表示法一:边列表#

核心设计:建两个列表——一个存所有顶点,一个存所有边。

顶点列表:仅仅是一个名字/字符串的列表。

边对象:一条边由其两个端点确定 → 定义为具有两个字段的结构体:起始顶点、终止顶点。

字段命名约定:

图类型字段命名原因
无向图第一顶点 / 第二顶点顺序不重要
有向图起始顶点 / 终止顶点顺序很重要,FH 与 HF 是两种不同连接

加权图 → 在边对象中再加一个字段存权重。

内存优化:

问题:若边对象前两个字段存字符串,各行长度不同,每行占用内存不一样
优化 1:字段存「指向顶点列表中那些名称的引用/指针」
→ 每行内存相同,且引用比名称副本省很多开销
优化 2(更好的做法):只存顶点在顶点列表中的「索引(整数)」
→ 边对象的两个字段变成两个整数字段
→ 边列表中每一行消耗的内存量都相同

空间复杂度:

顶点列表:O(|V|)
边列表(用索引/引用设计):O(|E|)
整体:O(|V| + |E|)

想在图的内存存储上做得比这更好很难。

时间复杂度才是问题所在:

操作做法复杂度
找某节点的所有邻居必须扫描整个边列表做线性搜索O(|E|)
判断两个节点是否相连同样要在线性搜索,最坏看所有条目O(|E|)

有向图只看起始节点;无向图起点和终点都要看。

为什么 O(|E|) 被认为很贵:

|E| 最坏 ≈ V²
任何以边数为量级运行的操作都被认为代价很高
我们希望把成本控制在「与顶点数量同量级」
O(V) 比 O(E) 好得多

表示法二:邻接矩阵#

核心设计:把边存在二维数组/矩阵中,大小 V × V。

每个顶点有一个 0 到 V−1 的索引(从顶点列表中取索引:A=0、B=1、C=2…)
无权图:A[i][j] = 1 表示存在从 i 到 j 的边,否则为 0

无向图 vs 有向图的矩阵:

矩阵性质每条边填几个位置
无向图对称(A[i][j] = A[j][i])两个位置(只需看一半)
有向图不对称一个位置(必须遍历整个矩阵)

时间复杂度:

操作条件复杂度
找邻居给的是名称O(|V|)(先扫顶点列表找索引 O(|V|) + 扫一行 O(|V|))
找邻居已知索引O(|V|)(省掉扫顶点列表)
判断两节点是否相连已知索引O(1) ✓
判断两节点是否相连给的是名称O(|V|)

关键在于:邻接矩阵判断相连不是无条件 O(1),给名称时还要先扫顶点列表。

优化:用额外内存建一个哈希表,名称和索引作为键值对,则「名称→索引」也变成 O(1)。

总评:用邻接矩阵,最常执行操作的时间成本与顶点数量成量级,而非与边数量成量级。

加权图的邻接矩阵:

A[i][j] 直接存该边的权重
不存在的边要设一个默认值,例如一个非常大或最大可能的整数值(图中填的是「无穷」)
「这种值绝不会被预期成为边的权重」

空间代价:

邻接矩阵确切使用 V² 个单位的内存 → 空间复杂度 O(V²)
矩阵不仅存了「相连」的信息,还存了「未相连」的冗余信息

示例图具体数字:边列表只消耗 10 行内存,而邻接矩阵消耗 64 个单位(8×8)。

Facebook 的具体演算:

假设有 10 亿(10⁹)用户
平均一名用户的朋友数不超过 1000
连接总数 = 10⁹ × 1000 = 10¹²
无向图要除以 2(否则每条边算两次)→ 5 × 10¹¹ 条边
这远远少于顶点数量的平方(10¹⁸)
邻接矩阵需要 10¹⁸ 个存储单元 ≈ 10¹⁸ 字节 ≈ 1000 PB(拍字节)
→ 绝不可能装在一块物理磁盘上
邻接表只要 5 × 10¹¹ 字节 = 0.5 TB(太字节)
→ 如今一台典型个人电脑就会有这么大的存储空间

结论:大多数真实世界的图都是稀疏的。邻接矩阵对多数频繁操作运行时间好,但空间效率不高,因此并不适用。

表示法三:邻接表#

核心思路:只保留「相连」的信息,摒弃「未相连」的信息(因为后者可以推导出来)。

不再用「数组索引代表边终点、值表示是否有边」,而是直接保存一个「所有与之相连的节点的列表/集合」。

语义上是反过来的:索引不再代表任何含义,值才是我们所连接到的节点的实际索引(与矩阵的语义正好相反)。

示例图的邻接表(8 顶点):

0 号节点连 1、2、3
1 号节点连 0、4、5
2 号节点连 0、6
…
7 号节点有 4 个连接(3、4、5、6)
各行的连接数分别是:0 号 3 个、1 号 3 个、2 号 2 个…… 每行大小都不同

如何用编程实现「每行不同大小」:

C/C++:创建一个「指针数组」,大小为 8
每个指针指向一个不同大小的一维数组
第 0 个指针 → 大小 3 的数组
第 7 个指针 → 大小 4 的数组

空间复杂度:

无向图:2 × |E| 个单位内存
有向图:|E| 个单位内存

严格表述是 O(|V| + |E|):因为存储顶点也会占用一定内存;但假如可以假定顶点数量相较于边数量少得多,就可以简单说与边的数量有关。能正确计数总是好事。

时间复杂度对比:

操作邻接矩阵邻接表
判断两节点是否相连O(1) ✓O(V)(无法直接定位,必须逐行扫描)
找某节点的所有邻居O(V)O(V)(最坏情况一行可能有 V 个单元)

邻接表的优化:若保持每行有序,可改用二分搜索,成本降到 O(log V),对数运行时间确实很棒,但始终维持数组有序在其他方面代价很高。

稀疏图下的实际性能:

社交网络有 10 亿(10⁹)用户
任何人拥有朋友的最大数量是 1 万(10⁴)
机器每秒能扫描/读取 10⁶ 个单元
操作邻接矩阵邻接表
找给定节点的所有邻居扫一整行 = 10⁹ 个单元 → 1000 秒 ≈ 16.66 分钟 ✗最多 10⁴ 个单元 → 10 ms ✓
判断两个节点是否相连读一个单元 → 1 微秒 ✓必须扫整行 → 10 ms

设计一个社交网络该选哪种结构?机器不能让用户等 16 分钟。

结论:对大多数真实世界的图,邻接表更好——既省空间又省时间。但前提是图必须是稀疏的。

动态增删边:

邻接矩阵邻接表(数组实现)
插入一条新边只需把某格的 0 改成 1 → O(1)要往那一行加一个元素,但动态增加现有数组的大小是不可能的 → 必须新建数组 + 复制旧内容 + 抹去旧数组 ✗
删除一条边把 1 改成 0 → O(1)同样麻烦

用数组实现动态或可变列表很棘手,这种「新建数组 + 复制旧数据」的做法代价很高,这正是我们常常改用链表存储动态/变化列表的确切原因。

邻接表的链表实现:

每行改成一个链表:不再需要动态改数组大小,插入和删除容易得多
实现:为每个节点创建一个链表存储其邻居
创建一个「指针数组」,唯一区别是每个指针指向一个「链表的头部」
链表节点有两个域:一个存数据,一个存下一个节点的地址
加权图 → 在节点里再加一个字段存权重

这种「把一个节点的邻居信息存储在链表中」的结构,正是通常所说的邻接表。

这里要分清什么是什么:指针数组的元素是指向节点的指针(只存地址);而带两个字段的那个才是节点。

一个延伸问题:在邻接表里添加/删除连接有多灵活?有没有办法改进?如果不用链表存储邻居,而是用二叉搜索树,会怎样?

会更好,因为查找、插入、删除一个邻居的时间成本都会降到对数级。

三种表示法完整对比#

操作边列表邻接矩阵邻接表
找某节点的所有邻居O(|E|)O(|V|)O(|V|) 最坏(有序时 O(log V))
判断两节点是否相连O(|E|)O(1)(仅当已知索引;给名称则 O(|V|))O(|V|) 最坏
增删边麻烦O(1) ✓数组实现麻烦,链表实现容易
空间O(|V|+|E|)O(V²)O(|V|+|E|)(无向 2|E|、有向 |E|)
适合简单存储密集图稀疏图 ✓

坑#

  • N 个节点的树恰好 N−1 条边,图没有这个限制;树是图的特例。有向图最大边数 N(N−1),无向图 N(N−1)/2,差一倍。
  • 「路径」默认指简单路径;允许重复的叫「漫步」,边不重复的叫「轨迹」。无向图说「连通」,有向图说「强连通」,用词要区分。
  • 邻接矩阵判断相连不是无条件 O(1),给名称时要先扫顶点列表;它的空间是 O(V²),稀疏图会浪费大量内存(Facebook 要 1000 PB)。
  • 邻接表的「找邻居」最坏也是 O(V),只有稀疏图才有实际优势;索引不代表含义,值才代表连接的节点(与矩阵语义相反)。
  • 邻接表用数组实现时增删边很麻烦(数组不能动态增长),所以实际用链表。大多数真实世界的图都是稀疏的,邻接表通常是更好的选择。
图
https://me.622168.xyz/posts/data-structure/17-graph/
作者
Shaw 的小屋
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0