408考研数据结构里,图的存储结构一直是一个“看着都会、一考就懵”的章节。邻接矩阵、邻接表还稍微好一点,等看到十字链表和邻接多重表的时候,很多同学直接选择战略性放弃,觉得“这两个结构又偏又难,考的概率不大”。但实际情况是,408统考和各大自命题院校都很喜欢在这两个结构上出选择题,有时候还会在大题里让你根据图画出对应的存储结构。一旦考到,这就是最单纯的送分题,因为考点非常固定:结构定义、适用场景、指针含义、时空复杂度、简单画图。只要花30分钟把原理理顺,这部分分数几乎是可以“白捡”的。
这篇文章就围绕“用最短时间拿下十字链表和邻接多重表”来写,我会先带你把前置知识串一遍,然后分别拆解两种结构每个字段的含义,再通过具体例子演示画法,最后给出考场答题话术、易错点和一套30分钟的复习计划。不管你是刚开始复习,还是已经进入冲刺阶段,都能直接拿来用。
1. 为什么十字链表和邻接多重表容易丢分
1.1 统考大纲到底考什么
408大纲对“图的存储结构”这一块的要求是:掌握邻接矩阵、邻接表、十字链表、邻接多重表的基本概念以及基本操作。要注意的是,这四个结构并不是平均用力的。邻接矩阵和邻接表当然最重要,十字链表和邻接多重表则属于“低频但稳定出现”的考点。常见考法就是选择题中问你“哪种结构适合有向图”“哪种结构求入度方便”“哪种结构适合无向图并便于删除边”,偶尔也会在综合题中出现“画出该图的十字链表/邻接多重表”这种小问。
很多同学丢分不是因为题目难,而是因为复习时直接跳过了这两个概念。等到考场上看到英文缩写或陌生字段名,第一反应就是“我不会”。实际上,十字链表和邻接多重表的知识点密度并不高,关键就几句话:十字链表服务于有向图,把邻接表和逆邻接表合在了一起;邻接多重表服务于无向图,让每条边只对应一个结点。把这两句话理解透,再配合画图训练,基本就能覆盖大部分考点。
1.2 丢分的三个典型原因
第一类是“结构适用范围”记忆混乱。十字链表里有“表头结点”和“弧结点”,邻接多重表里也有“表头结点”和“边结点”,很多人背着背着就把两者混了,最后变成“有向图用邻接多重表、无向图用十字链表”的错误结论。第二类是字段名太多,记不住。tailvex、headvex、hlink、tlink、ivex、jvex、ilink、jlink,八个字段放在一起,如果不理解每个字段为什么存在,死记硬背必然出错。第三类是只看文字不画图。考试如果只考概念判断,背一背可能还有用,但让你“根据图画出十字链表”的时候,光背定义是没有意义的,必须亲手画过一遍,知道指针怎么串起来。
这三点本质上是同一个问题:没有把存储结构理解成“数据结构”,而是把它理解成“名词解释”。数据结构必须有图、有指针、有具体例子,光背概念一定拿不到分。
1.3 这两个结构到底解决了什么问题
要理解十字链表和邻接多重表,先要理解它们出现的背景。邻接矩阵的优点是判断两个顶点之间是否有边非常快,但空间复杂度是O(n²),顶点多而边少的时候浪费严重。邻接表节省了空间,但有向图中只方便求“出度”,不方便求“入度”;无向图里同一条边会被存储两次,删除或标记一条边时非常麻烦。
十字链表就是为了解决“有向图入度难求”的问题,它把邻接表和逆邻接表合并到一张表里,一个弧结点同时参与出边链表和入边链表。邻接多重表则是为了解决“无向图存储两次边”的问题,每条边只用一个边结点,同时挂在两个顶点的边链表中。这两种结构本质上都是对邻接表缺点的“补丁”,理解了这个动机,后面的字段设计就顺理成章了。
2. 前置复习:邻接表与逆邻接表如何互补
2.1 邻接表只能“顺藤摸出边”
邻接表的思路是:为每个顶点建立一个单链表,链表里保存从该顶点出发能够直接到达的邻接点。对于无向图来说,每条边会被记录两次,比如边(v0,v1)既出现在v0的链表中,也出现在v1的链表中。对于有向图来说,邻接表存储的是“以当前顶点为弧尾”的弧,也就是出边。
所以邻接表在计算有向图顶点出度时非常方便,直接从该顶点的链表头开始遍历,数一下结点个数即可。但计算某个顶点的入度就麻烦了:因为入边全部散落在其他顶点的链表中,需要扫描所有顶点的所有边链表,才能数清楚到底有哪些弧指向当前顶点。这个操作的时间复杂度是O(n+e),在稠密图或需要大量求入度的场景下效率很低。
2.2 逆邻接表的方向反过来
逆邻接表正好反过来:每个顶点的链表里,存储的是“以当前顶点为弧头”的弧,也就是入边。这样一来,求入度变得很方便,直接数当前顶点链表里的结点个数即可,但求出度又要扫描全图。可以看到,邻接表和逆邻接表各自只解决了一个方向的问题,顾此失彼。
如果能把这两个方向合到一张表里,那就是十字链表。换句话说,十字链表并不是一个全新的概念,而是“邻接表 + 逆邻接表”的叠加,只是用一套结点把两套链表同时组织了起来。
2.3 两种表合并的思路
十字链表的做法是:每个顶点结点同时保存两个指针,firstout指向出边链表的第一条弧,firstin指向入边链表的第一条弧。每个弧结点同时保存弧尾下标、弧头下标,以及两个指针:一个用于把“弧尾相同”的弧串起来,另一个用于把“弧头相同”的弧串起来。这样,从任意一个顶点出发,既可以通过firstout沿着出边链表遍历所有出边,也可以通过firstin沿着入边链表遍历所有入边。
邻接多重表的思路类似,但它针对的是无向图。无向图中一条边不分方向,所以不需要“弧尾”“弧头”这种二元结构,只需要“一个边结点记录两个端点下标”。每个边结点通过两个指针分别挂在两个端点的边链表中,这样每条边只存储一次,但依然能被两个顶点同时访问到。理解了这条主线,后面的字段定义就容易记了。
3. 手把手看懂十字链表
3.1 十字链表的结构定义
十字链表,英文是Orthogonal List,是“正交链表”的意思,中文教材一般叫十字链表。它是有向图的一种链式存储结构,也可以看作邻接表和逆邻接表的结合体。在有向图中,一条弧有两个端点:弧尾和弧头。约定弧的方向是从弧尾指向弧头,例如弧<v,w>中,v是弧尾,w是弧头。很多同学在这里会搞反,记住“尾”在出发端,“头”在到达端,就和箭头的形状对应起来了:箭尾发出,箭头指向。
用C语言定义十字链表,通常包含弧结点、顶点结点和图结构三部分。弧结点中tailvex和headvex分别记录弧尾、弧头顶点在顶点表中的下标,hlink指向“弧头相同”的下一条弧,tlink指向“弧尾相同”的下一条弧,info存放权值等信息。顶点结点中firstin指向“以该顶点为弧头”的第一条弧,firstout指向“以该顶点为弧尾”的第一条弧。
#define MAX_VERTEX_NUM 20 typedef struct ArcNode { int tailvex, headvex; // 弧尾下标、弧头下标 struct ArcNode *hlink, *tlink; // 弧头相同的弧、弧尾相同的弧 int info; // 弧的信息,例如权值 } ArcNode; typedef struct VNode { char data; // 顶点信息 ArcNode *firstin, *firstout; // 入边链表头、出边链表头 } VNode; typedef struct { VNode xlist[MAX_VERTEX_NUM]; // 顶点表 int vexnum, arcnum; // 顶点数、弧数 } OLGraph;这个定义中的字段名与王道、严蔚敏教材基本一致,实际考试答题时只要把字段功能写清楚,名字略有差异是没问题的。
3.2 每个字段的含义
| 字段 | 所属结点 | 含义 | 记忆方法 |
|---|---|---|---|
| tailvex | 弧结点 | 弧尾顶点在顶点表中的下标 | tail = 弧尾 = 出发端 |
| headvex | 弧结点 | 弧头顶点在顶点表中的下标 | head = 弧头 = 到达端 |
| hlink | 弧结点 | 指向下一条“弧头相同”的弧 | h 与 head 对应 |
| tlink | 弧结点 | 指向下一条“弧尾相同”的弧 | t 与 tail 对应 |
| firstin | 顶点结点 | 指向以该顶点为弧头的第一条弧 | in = 入边 |
| firstout | 顶点结点 | 指向以该顶点为弧尾的第一条弧 | out = 出边 |
这里最核心的区分是“hlink管入边,tlink管出边”。因为hlink指向的弧和当前弧拥有相同的headvex,也就是共同到达同一个顶点,所以它属于入边链表;tlink把tailvex相同的弧串在一起,也就是共同从同一个顶点出发,所以它属于出边链表。顶点结点的firstin和firstout分别指向这两条链表的第一个结点,这样就实现了“入度、出度都能快速求”的目标。
3.3 一个具体例子:从有向图到十字链表
下面用一个有向图来演示十字链表的画法。顶点集合为{v0, v1, v2, v3},弧的输入顺序为:<v0,v1>、<v0,v2>、<v1,v2>、<v2,v3>、<v3,v0>。采用头插法建立十字链表时,后插入的弧会排在链表前面,这一点在考试中不影响正确性,因为十字链表不要求唯一的排列顺序,只要指针链完整即可。
按照输入顺序使用头插法,最终各顶点的指针为:
- v0.firstout指向<v0,v2>,沿着tlink可找到<v0,v1>;
- v0.firstin指向<v3,v0>,因为以v0为弧头的弧只有这一条;
- v1.firstout指向<v1,v2>;
- v1.firstin指向<v0,v1>;
- v2.firstout指向<v2,v3>;
- v2.firstin指向<v1,v2>,沿着hlink可找到<v0,v2>;
- v3.firstout指向<v3,v0>;
- v3.firstin指向<v2,v3>。
考试时如果要求画出完整十字链表,可以按以下步骤操作:第一,画顶点表,每个顶点写出data、firstin、firstout三个域;第二,为每条弧画一个弧结点,标注tailvex和headvex;第三,先串出边链表,把tailvex相同的弧通过tlink连起来;第四,再串入边链表,把headvex相同的弧通过hlink连起来。两个链表可以画成“十”字交叉的样子,这也是十字链表名称的由来。
3.4 出度和入度怎么求
十字链表最直观的好处就是求度和入度都很方便。计算顶点v的出度,只要从v.firstout开始,沿着tlink指针一路数下去,直到NULL为止。计算顶点v的入度,只要从v.firstin开始,沿着hlink指针一路数下去。
例如上面例子中,v2的出度为1,因为从v2.firstout出发只能遍历到<v2,v3>这一条弧;v2的入度为2,因为从v2.firstin出发,沿着hlink可以依次遍历到<v1,v2>和<v0,v2>两条弧。如果不用十字链表,用普通邻接表求v2的入度,需要扫描所有顶点的出边链表才能统计出来,效率差别非常大。这个对比也经常出现在选择题的选项描述中。
3.5 建立十字链表的算法
建立十字链表的过程并不复杂,核心就是“每读入一条弧,就生成一个弧结点,按头插法分别挂到对应顶点的出边链表和入边链表中”。代码如下:
void CreateDG(OLGraph *G) { int n, m, i, j, k; ArcNode *p; char v1, v2; scanf("%d %d", &n, &m); // 输入顶点数、弧数 G->vexnum = n; G->arcnum = m; for (i = 0; i < n; i++) { scanf(" %c", &G->xlist[i].data); G->xlist[i].firstin = NULL; G->xlist[i].firstout = NULL; } for (k = 0; k < m; k++) { scanf(" %c %c", &v1, &v2); // 弧 <v1, v2> i = LocateVex(G, v1); // 找到 v1 的下标 j = LocateVex(G, v2); // 找到 v2 的下标 p = (ArcNode *)malloc(sizeof(ArcNode)); p->tailvex = i; p->headvex = j; // 头插法插入出边链表 p->tlink = G->xlist[i].firstout; G->xlist[i].firstout = p; // 头插法插入入边链表 p->hlink = G->xlist[j].firstin; G->xlist[j].firstin = p; } }这段代码中最容易出错的地方是:插入出边链表时,使用的是“弧尾顶点”的firstout;插入入边链表时,使用的是“弧头顶点”的firstin。简单来说,出边看尾,入边看头。算法的时间复杂度是O(n+e),空间复杂度同样是O(n+e),这也是408真题中常考的时间复杂度结论。
4. 手把手看懂邻接多重表
4.1 邻接表存储无向图的痛点
对于无向图,普通邻接表会把每条边存储两次。以边(v0,v1)为例,它既会出现在v0的邻接链表中,也会出现在v1的邻接链表中。如果只是遍历邻接点,这样做问题不大;但如果需要删除一条边,比如删除(v1,v2),就必须同时到v1和v2两个链表中找到对应的边结点,分别进行删除操作。如果需要在图的遍历算法中标记某条边是否已被访问,也会遇到“一条边对应两个结点”的尴尬:标记了一个,还要找到另一个同步修改。
邻接多重表就是为无向图设计的改进结构。它的核心思想是:每条边只用一个边结点表示,这个结点同时记录两个端点下标,并且通过两个指针同时挂到两个顶点的边链表中。这样一来,一条边只占一份空间,却仍然能被两个顶点同时访问到。
4.2 邻接多重表的结构定义
邻接多重表,英文是Adjacency Multilist。它包含边结点、顶点结点和图结构三部分。边结点中mark用于标记该边是否被访问过,ivex和jvex记录边依附的两个顶点下标,ilink指向“依附于ivex顶点”的下一条边,jlink指向“依附于jvex顶点”的下一条边,info存放边的权值等信息。顶点结点中firstedge指向依附于该顶点的第一条边。
#define MAX_VERTEX_NUM 20 typedef struct EBox { int mark; // 访问标记,0表示未访问,1表示已访问 int ivex, jvex; // 边依附的两个顶点下标 struct EBox *ilink, *jlink; // 分别指向依附于ivex和jvex的下一条边 int info; // 边的信息,例如权值 } EBox; typedef struct VexBox { char data; // 顶点信息 EBox *firstedge; // 指向依附于该顶点的第一条边 } VexBox; typedef struct { VexBox adjmulist[MAX_VERTEX_NUM]; int vexnum, edgenum; // 顶点数、边数 } AMLGraph;这里要注意的是mark字段。很多同学会忽略它,但这个字段恰恰是邻接多重表的重要优势之一:由于每条边只有一个结点,想标记一条边是否被访问过,只需要修改一个mark值,不需要同时修改两个地方。这也是邻接多重表适合深度优先遍历、广度优先遍历以及删除边操作的原因。
4.3 每个字段的含义
| 字段 | 所属结点 | 含义 | 记忆方法 |
|---|---|---|---|
| mark | 边结点 | 标记边是否被访问过 | visited标记 |
| ivex | 边结点 | 边依附的第一个顶点下标 | i = 第一个顶点 |
| jvex | 边结点 | 边依附的第二个顶点下标 | j = 第二个顶点 |
| ilink | 边结点 | 指向依附于ivex的下一条边 | i link 跟着 ivex |
| jlink | 边结点 | 指向依附于jvex的下一条边 | j link 跟着 jvex |
| firstedge | 顶点结点 | 指向依附于该顶点的第一条边 | 类似邻接表的first |
有一个容易混淆的地方是有读者会把十字链表和邻接多重表搞混。十字链表处理的是有向图,所以有“弧尾”“弧头”两个方向字段;邻接多重表处理的是无向图,所以只有“两个端点”,没有方向。答题时先判断图的类型,再决定使用哪种结构,思路就会清晰很多。
4.4 一个具体例子:从无向图到邻接多重表
下面用一个无向图来演示邻接多重表。顶点集合为{v0, v1, v2, v3},边集为{(v0,v1), (v0,v2), (v1,v2), (v2,v3)},输入顺序同上,采用头插法。
最终各顶点的firstedge为:
- v0.firstedge指向(v0,v2),沿着ilink可以找到(v0,v1);
- v1.firstedge指向(v1,v2),沿着ilink可以找到(v0,v1);
- v2.firstedge指向(v2,v3),沿着ilink可以找到(v1,v2),再沿着jlink可以找到(v0,v2);
- v3.firstedge指向(v2,v3),jlink为NULL。
可以看到,v0这条顶点链中,(v0,v2)的ivex是0,(v0,v1)的ivex也是0,所以它们之间通过ilink连接。而v2这条链中,(v2,v3)的ivex是2,所以它通过ilink连接到下一个依附于v2的边(v1,v2);(v1,v2)的jvex是2,所以它通过jlink连接到下一个依附于v2的边(v0,v2)。也就是说,在同一个顶点的边链表中,当前边到底使用ilink还是jlink,取决于该边是以ivex还是jvex的身份依附于当前顶点的。
画邻接多重表时,建议按以下步骤操作:第一,画顶点表,每个顶点包含data和firstedge;第二,为每条边画一个边结点,填写mark、ivex、jvex;第三,把每个边结点分别插入它两个端点的边链表;第四,检查每个顶点的边链是否包含所有依附于该顶点的边,确认没有遗漏。
4.5 建立邻接多重表的算法
建立邻接多重表的核心逻辑与十字链表很像:每输入一条边,就创建一个边结点,然后把它分别头插到两个端点的边链表中。
void CreateAMLGraph(AMLGraph *G) { int n, m, i, j, k; EBox *p; char v1, v2; scanf("%d %d", &n, &m); // 输入顶点数、边数 G->vexnum = n; G->edgenum = m; for (i = 0; i < n; i++) { scanf(" %c", &G->adjmulist[i].data); G->adjmulist[i].firstedge = NULL; } for (k = 0; k < m; k++) { scanf(" %c %c", &v1, &v2); // 无向边 (v1, v2) i = LocateVex(G, v1); j = LocateVex(G, v2); p = (EBox *)malloc(sizeof(EBox)); p->mark = 0; p->ivex = i; p->jvex = j; // 头插法插入 i 顶点的边链表 p->ilink = G->adjmulist[i].firstedge; G->adjmulist[i].firstedge = p; // 头插法插入 j 顶点的边链表 p->jlink = G->adjmulist[j].firstedge; G->adjmulist[j].firstedge = p; } }这段代码中有一个细节值得注意:当输入的是边(v1,v2),且i不等于j时,新边结点会同时出现在两个顶点的链表中。如果题目要求处理自环,也就是i和j相等时,需要单独设计指针链接方式,但408统考中基本不会涉及这种特殊情况,复习时不必过度纠结。
4.6 删除一条边为什么更快
邻接表存储无向图时,删除一条边需要操作两个链表。例如删除边(v1,v2),必须在v1的链表中找到(v1,v2)结点,还需要在v2的链表中找到另一个(v1,v2)结点,两个结点都要摘除,同时还要维护各自链表前驱结点的指针。如果之前某条边被标记或访问过,两个结点之间的同步也是一个麻烦事。
邻接多重表删除边时,只需要找到唯一的那个边结点,然后把它从ivex所在链表和jvex所在链表中分别摘下即可。虽然仍然需要调整两个链表,但只需要处理一个边结点,而且mark字段可以直接标识“已删除”状态,避免重复查找。正因为这一点,408中经常出现“无向图删除边、标记边时,使用邻接多重表比邻接表更方便”的判断选项。
5. 对比速记:一张表、四句话、一段答题话术
5.1 一图流对比表
| 对比维度 | 十字链表 | 邻接多重表 |
|---|---|---|
| 适用图 | 有向图 | 无向图 |
| 顶点结点指针 | firstin、firstout | firstedge |
| 弧/边结点关键字段 | tailvex、headvex、hlink、tlink | mark、ivex、jvex、ilink、jlink |
| 核心能力 | 方便求出度、入度 | 方便标记边、删除边 |
| 边的存储方式 | 每条弧一个结点,同时参与出边链和入边链 | 每条边一个结点,同时挂到两个端点的边链 |
| 空间复杂度 | O(n+e) | O(n+e) |
| 主要优势 | 解决有向图入度难求问题 | 解决无向图边重复存储问题 |
这张表可以在复习时自己默写一遍,能够默写出来,说明概念基本过关了。注意十字链表里没有mark字段,邻接多重表里没有tailvex和headvex字段,这两个区别是选择题的高频“陷阱”。
5.2 四句话记忆法
第一句:十字链表属于有向图,邻接多重表属于无向图。这是所有记忆的前提。第二句:十字链表中,tail和firstout管出边,head和firstin管入边。一个“出”一个“入”,方向不要搞反。第三句:邻接多重表每条边只存一次,ilink跟着ivex走,jlink跟着jvex走。多念几遍就能记住。第四句:两种结构的空间复杂度都是O(n+e),都适合存储稀疏图。考试时看到“空间复杂度是O(n²)”的选项可以直接排除。
5.3 考场答题规范话术
如果选择题要求解释“为什么十字链表能方便求入度和出度”,可以这样写:十字链表是邻接表和逆邻接表的结合,每个弧结点同时保存弧尾下标tailvex和弧头下标headvex,并通过tlink将弧尾相同的弧串成出边链表,通过hlink将弧头相同的弧串成入边链表。因此从某顶点的firstout出发可以遍历所有出边,求出度;从firstin出发可以遍历所有入边,求入度。
如果题目问“为什么邻接多重表适合无向图的边删除和标记”,可以这样写:邻接多重表中的每一条边只用一个边结点表示,边结点通过ilink和jlink同时挂到两个端点的边链表中。由于边结点唯一,标记边只需要修改一个mark字段,删除边只需要找到这一个结点并把它从两个链表中摘下,避免了邻接表中同一条边存储两次带来的同步问题。
这两段话几乎可以直接背下来,考试时根据题目要求适当删减,就是很好的作答框架。
6. 408常见考法与丢分陷阱
6.1 选择题高频判断
下面是几道常考的判断类选择题,可以先自己思考答案,再看解析。
判断题1:十字链表既适用于有向图,也适用于无向图。答案是错。十字链表中弧结点有tailvex和headvex,这是有向图弧的两个端点概念,无向图没有弧头和弧尾的区分。
判断题2:邻接多重表是十字链表的一种特殊形式。答案是错。两者结构类似,但服务对象不同:十字链表服务于有向图,邻接多重表服务于无向图,不能混为一谈。
判断题3:十字链表可以方便地求出有向图顶点的入度和出度。答案是对。这正是十字链表相对普通邻接表的优势。
判断题4:对于同一个无向图,邻接多重表的边结点个数比邻接表少。这个选项经常出现,严格来说邻接表把每条边存了两次,所以边结点数量约为邻接多重表的两倍。如果题目表述是“同一张无向图,邻接多重表边结点数是邻接表边链表结点总数的一半”,就是对;如果只是说“边结点更少”,通常也认为正确。
判断题5:十字链表和邻接多重表的空间复杂度都是O(n+e)。答案是对。n为顶点数,e为边或弧的数量,空间开销比邻接矩阵的O(n²)小。
6.2 手写存储结构示意题
考试画十字链表或邻接多重表时,阅卷重点通常有三个。第一,结构框架完整:顶点表要写清data和指针域,不能只画边结点不画顶点表。第二,指针链不丢:每个弧结点或边结点都必须出现在对应的链表里,确保遍历时能从表头出发访问到所有边。第三,字段值正确:tailvex、headvex、ivex、jvex的下标必须和顶点表对应,如果下标写反,即使指针画对了也会扣分。
还有一个很重要的考试经验:十字链表和邻接多重表的画法并不唯一。你用头插法得到的结果,和教材上可能长得不一样,但只要每个指针的含义正确,边的链接关系完整,就是正确答案。不要在考场上强迫自己回忆“标准画法”,浪费时间的概率很大。
6.3 算法设计题答题模板
如果遇到“设计十字链表或邻接多重表的存储结构,并实现建表算法”这种算法题,可以直接套用以下模板。第一步,定义顶点结点和弧/边结点结构体。第二步,初始化顶点表,将所有指针域置空。第三步,循环读入每条弧或边,定位两个端点在顶点表中的下标。第四步,生成新结点,填写相关字段。第五步,用头插法将新结点插入到对应链表中。第六步,输出或返回图结构,算法结束。
在解释复杂度时,建立十字链表和邻接多重表的时间复杂度都是O(n+e),空间复杂度也是O(n+e)。把这个复杂度结论写上,也能获得过程分。如果题目还要求写出针对具体图的存储结果,不要忘记在算法之外单独画出顶点表和边链表。
7. 30分钟无痛复习计划
7.1 时间分配表
| 时间段 | 任务 | 目标 |
|---|---|---|
| 0-5分钟 | 回顾邻接表和逆邻接表 | 理解出边、入边的基本概念 |
| 5-15分钟 | 画2遍十字链表 | 熟练画出顶点表、弧结点、出边链、入边链 |
| 15-25分钟 | 画2遍邻接多重表 | 熟练画出顶点表、边结点、两个边链表 |
| 25-30分钟 | 做3道自测题 | 查漏补缺,重点看易错点清单 |
这套计划的核心不是“看”,而是“画”。只要亲手画上两遍,很多容易混淆的字段关系会自然理顺。如果时间充裕,可以再多画一组不同的图,直到不需要看书也能默写结构体定义。
7.2 三道自测题与解析
自测题1:有一个有向图,顶点为v0、v1、v2,弧为<v0,v1>、<v1,v2>、<v2,v0>。请画出它的十字链表结构。解析:这是一个三顶点三弧的环,v0的firstout指向<v0,v1>,v1的firstout指向<v1,v2>,v2的firstout指向<v2,v0>;同时v0的firstin指向<v2,v0>,v1的firstin指向<v0,v1>,v2的firstin指向<v1,v2>。每个顶点的出度、入度都是1。
自测题2:邻接多重表主要适用于存储哪类图?答案是无向图。它把一个无向边只存储为一个边结点,便于边的删除和标记操作,解决了邻接表存储无向图时边重复的问题。
自测题3:如果要在无向图中频繁删除边,应该优先选择哪种存储结构?答案是邻接多重表。因为每条边只有一个边结点,删除操作只需要调整两个顶点的链表指针,不需要像邻接表那样处理两个重复的边结点。
7.3 考前易错点清单
考前再把下面几条过一遍。第一,有向图用十字链表,无向图用邻接多重表,不要互换。第二,<v,w>中v是弧尾,w是弧头;tail对应出发端,head对应到达端。第三,十字链表中firstout和tlink处理出边,firstin和hlink处理入边。第四,邻接多重表中ilink和jlink分别跟随ivex和jvex,画图前先确定当前边是以哪个顶点身份挂入链表的。第五,两个结构的空间复杂度都是O(n+e),不是O(n²)。第六,画图时下标从0开始,不要和顶点编号搞混。
8. 总结与后续复习建议
十字链表和邻接多重表在408数据结构中属于“投入产出比”很高的考点。它们不像最短路径、拓扑排序那样需要大量算法训练,核心就是四种存储结构之间的对比与选择。只要沿着“有向图用十字链表,无向图用邻接多重表”这条主线,把字段含义和指针关系通过画图理解清楚,选择题基本不会错,大题中的画图小问也能稳稳得分。
复习完这两个结构后,建议回到整个“图”章节,把邻接矩阵、邻接表、十字链表、邻接多重表放到一起,从空间复杂度、求度便利性、删除边便利性、适用图类型四个维度做一次对比。你会发现它们之间并不是孤立的,而是各自解决不同的问题。之后再去做图的遍历、最小生成树、最短路径题目时,对“题目给出的存储结构会影响算法复杂度”这句话会有更深的体会。
如果你按照本文的30分钟计划动手画了两遍图,相信下次再看到十字链表和邻接多重表,就不会再想直接跳过了。也可以把这篇文章收藏备用,考前30分钟翻出来,对照易错点清单快速过一遍,比翻厚厚的教材更高效。