Appearance
十字链表与邻接多重表
2026 大纲 五(二)图的存储及基本操作 3. 邻接多重表、十字链表(前置是《邻接表》)。
这两个结构是来补邻接表的两处短板的
邻接表有两个毛病,各自只在一种图上发作。
有向图上的毛病是求入度。 顶点
无向图上的毛病是"一条边有两个分身"。 边 free 一次;给边打个"已访问"标记,只标到其中一个分身;统计边权和时每条边会被算两遍。
两个结构的解法是同一个思想:让一个边(弧)结点同时挂在两条链上,而不是存两份。
🔴 两者不可互换,判据不是死记名字,是看图有没有方向。看结点域也能认出来:有
tailvex/headvex(分头尾)的是十字链表,有ivex/jvex(不分主次)的是邻接多重表。
空间上,两者都仍是
先动手看一眼
拨的时候盯住一件事:从两个不同的顶点射出的箭头,落在同一个方框上。那个方框就是被共享的那一份边(弧)结点——这两个结构的全部价值都在这个图像里。
十字链表:一个弧结点,两条链
弧结点从自己身上引出两个指针:
tlink指向"弧尾相同的下一条弧"——顺着它走,走的是同一个顶点的出弧链。hlink指向"弧头相同的下一条弧"——顺着它走,走的是同一个顶点的入弧链。
两条链在结点处交叉,"十字"就是这么来的。顶点结点也相应地有两个指针 firstout 和 firstin。
于是求度变成沿两条链各数一遍,各自
c
for (ArcBox *p = G->xlist[v].firstout; p; p = p->tlink) outD++; // 出度
for (ArcBox *p = G->xlist[v].firstin; p; p = p->hlink) inD++; // 入度⚠️ 两条链的推进指针不同,写反不会崩,只会沿错误的链跑出错误结果。 记法是首字母对齐:
firstout配tlink(out—tail,出弧的弧尾是我)、firstin配hlink(in—head,入弧的弧头是我)。
插弧的代码就是"一次 malloc,两次头插",各
c
void insertArc(OLGraph *G, int i, int j) {
ArcBox *p = (ArcBox *)malloc(sizeof(ArcBox));
p->tailvex = i; p->headvex = j; p->info = NULL;
p->tlink = G->xlist[i].firstout; G->xlist[i].firstout = p; // 插 i 的出弧链
p->hlink = G->xlist[j].firstin; G->xlist[j].firstin = p; // 插 j 的入弧链
G->arcnum++; // 两条链共享同一个 p —— 关键就在这一步
}最后那行注释是重点:两条链共享同一个 p,不是两份数据。所以
邻接多重表:一条边只有一个结点
无向边没有方向,不需要区分入与出,所以顶点结点只有一个指针 firstedge。边结点则记两个端点 ivex、jvex(不分主次)和两个后继指针 ilink、jlink,外加一个 mark 标志域。
这里有一处必须想清楚,否则代码一定写错:
🔴 遍历依附于
的边时,走 ilink还是jlink,取决于"当前顶点是这条边的哪一端"。
c
// 遍历依附于顶点 v 的所有边(通用模板,推进必须判端)
for (EBox *p = G->adjmulist[v].firstedge; p != NULL;
p = (p->ivex == v) ? p->ilink : p->jlink) {
// 对端顶点 = (p->ivex == v) ? p->jvex : p->ivex
}为什么不能固定写 p = p->ilink?因为同一个边结点被两条链共享,它的 ilink 属于 ivex 那条链、jlink 属于 jvex 那条链。你从 p->ilink 会从一个顶点的链跳到另一个顶点的链上,结果既漏边又串边。
mark 和 visited[] 也不是冗余的两套标志:mark 标记的是边,visited[] 标记的是顶点。只留 mark,有环时顶点会被重复访问;只留 visited[],统计边权和时每条边仍会被算两次。两者各管一件事。
数一个顶点的度:沿链走,一步一步判端
这是这两个结构上最基本的操作,也是它们目前唯一被真题直接问过的形式。做法只有一句话:顶点
难点全在"沿链走"这个动作上。题面通常把每个顶点的链按
b: e3 e1
d: e5 e7 e2 e3其中
数
看起来平淡,但错法有两种,而且都会被摆成选项:
- 只数了"以该顶点为
ivex"的那些边。在 里是 ivex,在、 、 里却是 jvex。只认一端就把 4 数成 1。 - 推进时走错指针,中途跳到别人的链上。 从
走到 要用 ilink(因为是 的 ivex),但从继续走要用 jlink(是 的 jvex)。固定走一种指针,会数出一个不多不少但完全错误的数字。
两条错法的根都是同一个:边结点不分主次,ivex/jvex 只是记录顺序,不代表任何身份。
十字链表建表走查:四条弧怎么同时进两条链(想手画一遍就展开)
有向图
| 插入 | ||||||
|---|---|---|---|---|---|---|
| 初始 | ∧ | ∧ | ∧ | ∧ | ∧ | ∧ |
| ∧ | ∧ | ∧ | ∧ | |||
| ∧ | ∧ | ∧ | ||||
| ∧ | ∧ | |||||
读出度入度:
再数一遍结点:整张表只有 4 个弧结点,与弧数相同。若改用"邻接表 + 逆邻接表",两张表各 4 个共 8 个。
结点结构的完整定义:
c
typedef struct ArcBox { // 十字链表:弧结点
int tailvex, headvex;
struct ArcBox *hlink, *tlink; // 弧头相同、弧尾相同的弧的链域
InfoType *info;
} ArcBox;
typedef struct VexNode {
VertexType data;
ArcBox *firstin, *firstout; // 第一条入弧、第一条出弧
} VexNode;
typedef struct { VexNode xlist[MAX_VERTEX_NUM]; int vexnum, arcnum; } OLGraph;
图注:左边是含 5 个顶点、6 条弧的有向图。右边的 Nodetable 是顶点表,每个顶点有 两个指针列(firstin 与 firstout);右侧 EdgeLinkedList 里的 6 个方框就是 6 个弧结点, 数量与弧数完全相同,没有任何一条弧被存两次。 图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.11 有向图的十字链表表示,p363
邻接多重表建表走查:为什么推进指针不能固定写 ilink
无向图 4 个顶点、4 条边,按
| 插入 | ||||
|---|---|---|---|---|
| 初始 | ∧ | ∧ | ∧ | ∧ |
| ∧ | ∧ | |||
| ∧ | ||||
| ∧ | ||||
把每一步走的是哪个指针标出来(∧ 表示 NULL):
V0.firstedge → e2(0,2) ─ilink→ e1(0,1) ─ilink→ ∧
V1.firstedge → e3(1,2) ─ilink→ e1(0,1) ─jlink→ ∧
V2.firstedge → e4(2,3) ─ilink→ e3(1,2) ─jlink→ e2(0,2) ─jlink→ ∧
V3.firstedge → e4(2,3) ─jlink→ ∧全图只有 4 个边结点(邻接表要 8 个),但四条顶点链加起来经过了 8 次,因为每个结点被两条链各穿过一次。
⚠️ 不能写成
p = p->ilink。反例就在链上:从 出发, 的 ivex是 2,走ilink到;但 的 ivex是 1 不是 2,它的ilink指向的是链上的下一条边 ,而不是 链上的 。硬写 p->ilink会从的链上跳到 的链上。
结点结构的完整定义:
c
typedef struct EBox { // 邻接多重表:边结点
int mark; // 访问标记(边级别)
int ivex, jvex;
struct EBox *ilink, *jlink; // 依附这两个顶点的下一条边
InfoType *info;
} EBox;
typedef struct { VertexType data; EBox *firstedge; } VexBox;
typedef struct { VexBox adjmulist[MAX_VERTEX_NUM]; int vexnum, edgenum; } AMLGraph;
图注:左边的无向图有 4 个顶点、3 条边。右边 NodeTable 每个顶点只有一个指针 (对比十字链表的两个——无向边没有方向,不需要区分入与出); Edge Linked List 里只有 3 个边结点,与边数相等,而同一个图用邻接表要 6 个。 图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.10 无向图的邻接多重表表示,p362
删边只删一个结点,与 mark 标志位怎么用(写这两段代码时展开)
删除边 free 一次:
c
// 把边结点 e 从顶点 v 的边链表中摘除
void detach(AMLGraph *G, int v, EBox *e) {
EBox *p = G->adjmulist[v].firstedge, *pre = NULL;
while (p != e) { // 沿 v 的链找 e 的前驱
pre = p;
p = (p->ivex == v) ? p->ilink : p->jlink; // 推进同样要判端
}
EBox *next = (e->ivex == v) ? e->ilink : e->jlink;
if (pre == NULL) G->adjmulist[v].firstedge = next; // e 是链头
else if (pre->ivex == v) pre->ilink = next; // 前驱经 ilink 链到 e
else pre->jlink = next; // 前驱经 jlink 链到 e
}
void deleteEdge(AMLGraph *G, int i, int j, EBox *e) {
detach(G, i, e);
detach(G, j, e);
free(e); // 只 free 一次 —— 这就是"删边只删一个结点"
G->edgenum--;
}修改前驱指针时同样要判端:pre 是经 ilink 还是 jlink 链到 e,取决于 pre 的哪一端是 free 一次,而且两个结点内容相同、地址不同,无法用"是不是同一个指针"配对,只能靠比对顶点编号。
需要对每条边恰好处理一次的操作(输出所有边、统计边权和、给边打标记),多重表把 mark 置位即可:
c
void DFS(AMLGraph *G, int v) {
visited[v] = 1; // 顶点标志照常要有
for (EBox *p = G->adjmulist[v].firstedge; p != NULL;
p = (p->ivex == v) ? p->ilink : p->jlink) {
if (!p->mark) {
p->mark = 1; // 处理过了,从另一端进来时不再处理
int u = (p->ivex == v) ? p->jvex : p->ivex;
if (!visited[u]) DFS(G, u);
}
}
}邻接表上要做同样的事只能靠额外手段:开一张"边是否已处理"的表(费空间),或者约定"只处理
四种图存储结构对比
| 存储结构 | 适用图 | 空间 | 求出度 | 求入度 | 判相邻 | 删一条边 |
|---|---|---|---|---|---|---|
| 邻接矩阵 | 有向 / 无向 | |||||
| 邻接表 | 有向 / 无向 | 无向要删 2 个结点 | ||||
| 十字链表 | 有向图 | 1 个结点,摘 2 条链 | ||||
| 邻接多重表 | 无向图 | — | — | 1 个结点,摘 2 条链 |
(无向图不分出入度,故邻接多重表求度为
考点速记
三条结论:
- 十字链表 = 有向图;邻接多重表 = 无向图。 前者为求入度而生,后者为"边只存一份"而生。
- 共同思想是"一个边/弧结点挂在两条链上",空间仍
。 - 每次推进都要先判"当前顶点是这条边的哪一端"——十字链表靠
firstout/tlink与firstin/hlink配对,邻接多重表靠ivex==v决定走ilink还是jlink。
这一节在真题里被考过的形式(下方「真题练习」逐题对应):
- 给一张邻接多重表,求指定两个顶点的度:题面把每个顶点的边链按
编号列出,你要做的就是数链上的结点个数。四个选项分别对应"两个都数对"和三种数错的组合——只认 ivex一端、走错推进指针、把两个顶点的答案对调。数之前先把每条依附的两个端点抄在旁边,再逐链核对。
易错:十字链表只用于有向图,邻接多重表只用于无向图。 拿到题先看图有没有方向;看结点域也能认:分
tailvex/headvex的是十字链表,ivex/jvex不分主次的是邻接多重表。
易错:
ivex/jvex只是记录顺序,不代表身份。 数某个顶点的度时,它可能出现在某些边的ivex位置、另一些边的jvex位置,两边都得算。
易错:推进指针不能固定。 十字链表求出度走
tlink、求入度走hlink;邻接多重表要按"当前顶点是哪一端"在ilink/jlink之间切换。固定走一种,会从一个顶点的链跳到另一个顶点的链上。
教材出处
- 严蔚敏《数据结构(C 语言版)》(第 2 版)印刷 p158,§6.4.3: "十字链表(Orthogonal List) 是有向图的另一种链式存储结构。可以看成是将有向图的邻接表 和逆邻接表结合起来得到的一种链表";同页给出弧结点 5 个域 (
tailvex、headvex、hlink、tlink、info)与顶点结点 3 个域的定义。 - 同书印刷 p159:十字链表的 C 语言存储表示
OLGraph; "建立十字链表的时间复杂度和建立邻接表是相同的……既容易找到以为尾的弧, 也容易找到以 为头的弧,因而容易求得顶点的出度和入度"。 - 同书印刷 p159,§6.4.4: "在邻接表中每一条边
有两个结点,分别在第 个和第 个链表中, 这给某些图的操作带来不便。例如……对已被搜索过的边作记号或删除一条边等, 此时需要找到表示同一条边的两个结点"——这正是邻接多重表的设计动机。 - 同书印刷 p160:边结点 6 个域(
mark、ivex、ilink、jvex、jlink、info) 与顶点结点 2 个域;"除了在边结点中增加一个标志域外,邻接多重表所需的存储量和邻接表相同"。
图源:殷人昆《数据结构——用面向对象方法与 C++ 描述》(第 2 版)图8.11(p363)、图8.10(p362)。
相关知识
邻接表(本篇出发点,两处短板即设计动机)| 邻接矩阵(判边 mark 与 visited[] 两套标志)