Skip to content

十字链表与邻接多重表

核心思想

十字链表(有向图)

十字链表的核心思路是为每个建立一个弧结点,同时将它链入两条链表——以该弧的弧头顶点为头的链表和以弧尾顶点为头的链表,形成"十字"交叉结构。

弧结点结构

| tailvex | headvex | hlink | tlink | info |
字段含义
tailvex弧尾顶点编号(起点)
headvex弧头顶点编号(终点)
hlink指向弧头相同的下一条弧
tlink指向弧尾相同的下一条弧
info弧的权值等信息(可选)

顶点结点结构

| data | firstin | firstout |
字段含义
data顶点数据
firstin指向以该顶点为弧头的第一条弧(入弧)
firstout指向以该顶点为弧尾的第一条弧(出弧)

结构示意(有向图:V₀→V₁, V₀→V₂, V₂→V₁):

顶点数组
[0] V₀  firstin=NULL       firstout → (0,1) → (0,2) → NULL  (tlink 链)
[1] V₁  firstin → (0,1) → (2,1) → NULL  (hlink 链)   firstout=NULL
[2] V₂  firstin → (0,2) → NULL           firstout → (2,1) → NULL

沿 firstout + tlink 可遍历某顶点的所有出弧(求出度);沿 firstin + hlink 可遍历某顶点的所有入弧(求入度)。

邻接多重表(无向图)

邻接多重表的核心思路是每条边只用一个边结点表示,该结点同时被它关联的两个顶点的链表共享。

边结点结构

| mark | ivex | ilink | jvex | jlink | info |
字段含义
mark标志位,标记该边是否被访问过
ivex边的一个顶点编号
ilink指向依附于 ivex 的下一条边
jvex边的另一个顶点编号
jlink指向依附于 jvex 的下一条边
info边的权值等信息(可选)

顶点结点结构

| data | firstedge |

每个顶点存储一个 firstedge 指针,指向第一条依附于该顶点的边。

关键特点:同一个边结点出现在两个顶点的边链表中,删除一条边只需删除一个结点,比邻接表高效。

交互可视化

通过下方的交互动画,你可以逐步观察十字链表与邻接多重表的执行过程:

加载可视化中...

操作详解

十字链表

建立十字链表

  1. 创建顶点数组,将每个顶点的 firstinfirstout 初始化为 NULL
  2. 对每条弧 <Vi, Vj>,创建弧结点,设置 tailvex=iheadvex=j
  3. 将弧结点用头插法分别插入 Vi 的出弧链表(tlink 链)和 Vj 的入弧链表(hlink 链)

求顶点度

  • 出度:沿 firstout 出发,沿 tlink 遍历,计数即可
  • 入度:沿 firstin 出发,沿 hlink 遍历,计数即可
  • 度 = 出度 + 入度

十字链表的优点

  • 既能方便地求出度,也能方便地求入度(邻接表只能方便求出度)
  • 空间复杂度 O(V+E),与邻接表相同
  • 建立十字链表的时间复杂度为 O(V+E)

邻接多重表

结构定义(C 语言)

c
typedef struct EdgeNode {
    bool mark;                // 访问标志(边级别,遍历时用)
    int ivex, jvex;           // 边的两个端点编号
    struct EdgeNode *ilink;   // 依附于 ivex 的下一条边
    struct EdgeNode *jlink;   // 依附于 jvex 的下一条边
    // InfoType *info;        // 权值等信息(可选)
} EdgeNode;

typedef struct {
    VertexType data;
    EdgeNode *firstedge;      // 第一条依附于该顶点的边
} VexNode;

VexNode adjMulList[MAXV];

建立邻接多重表

  1. 创建顶点数组,将每个顶点的 firstedge 初始化为 NULL
  2. 对每条边 (Vi, Vj),创建一个边结点,设置 ivex=ijvex=j
  3. 将边结点用头插法插入 Vi 的边链表(通过 ilink)和 Vj 的边链表(通过 jlink)

手画示例(这是理解"一个结点挂两条链"的关键):

无向图有 4 个顶点、4 条边,按 e1=(0,1)、e2=(0,2)、e3=(1,2)、e4=(2,3) 的顺序头插建表,四条顶点链如下(∧ 表示 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 个);② 同一个结点 e1 既在 V0 的链上又在 V1 的链上——从 V0 进来时 V0 是它的 ivex 端,沿 ilink 走;从 V1 进来时 V1 是它的 jvex 端,沿 jlink 走。走哪条链取决于"当前顶点是这条边的哪一端",这是邻接多重表所有操作的统一套路:

c
// 遍历依附于顶点 v 的所有边(通用模板)
for (EdgeNode *p = adjMulList[v].firstedge; p != NULL;
     p = (p->ivex == v) ? p->ilink : p->jlink) {
    // 处理边 p,对端顶点 = (p->ivex == v) ? p->jvex : p->ivex
}

删除边操作

邻接多重表删除边 (Vi, Vj) 只需把同一个结点从两条链上分别摘除,再 free 一次:

c
// 把边结点 e 从顶点 v 的边链表中摘除
void detach(int v, EdgeNode *e) {
    EdgeNode *p = adjMulList[v].firstedge, *pre = NULL;
    while (p != e) {                          // 找 e 在 v 链中的前驱
        pre = p;
        p = (p->ivex == v) ? p->ilink : p->jlink;
    }
    EdgeNode *next = (e->ivex == v) ? e->ilink : e->jlink;
    if (pre == NULL)
        adjMulList[v].firstedge = next;       // e 是链头
    else if (pre->ivex == v)
        pre->ilink = next;                    // 前驱经 ilink 链到 e
    else
        pre->jlink = next;                    // 前驱经 jlink 链到 e
}

// 删除边 (i, j)
detach(i, e);
detach(j, e);
free(e);     // 只 free 一次——这就是"删边只删一个结点"

对比邻接表:邻接表中无向边 (Vi, Vj) 对应两个独立的边结点,删边要在两条链里分别找到并 free 两次,还要保证两边删干净不残留。

mark 标志位的作用

需要对每条边只处理一次的操作(输出所有边、统计边权和、给每条边做标记)中,邻接多重表把 mark 置位即可——因为每条边本来就只有一个结点。邻接表里同一条边有两个分身,要么开一张额外的"边已处理"表,要么靠 i < j 之类的约定去重。

c
// 在邻接多重表上对每条边恰好处理一次(以 DFS 框架为例)
void DFS(int v) {
    visited[v] = true;                        // 顶点标志照常要有
    for (EdgeNode *p = adjMulList[v].firstedge; p != NULL;
         p = (p->ivex == v) ? p->ilink : p->jlink) {
        if (!p->mark) {
            p->mark = true;                   // 这条边处理过了,另一端不再处理
            int u = (p->ivex == v) ? p->jvex : p->ivex;
            if (!visited[u]) DFS(u);
        }
    }
}

易错:mark 是的访问标志,不能代替顶点的 visited[]——两个标志各管各的:visited[] 防止顶点重复访问,mark 防止同一条边经由两个端点被处理两次。

复杂度分析

四种图存储结构对比

存储结构适用图类型空间复杂度求度判断边是否存在删边特点
邻接矩阵有向图/无向图O(V²)O(V)O(1)O(1)适合稠密图,空间浪费大
邻接表有向图/无向图O(V+E)出度 O(1)遍历;入度需遍历整表O(V)需删两个结点(无向图)适合稀疏图
十字链表有向图O(V+E)出度/入度均可快速求O(V)O(V)邻接表 + 逆邻接表的结合
邻接多重表无向图O(V+E)O(度数)O(V)只需删一个结点每条边仅一个结点,删边方便

建表时间复杂度

存储结构建表时间
邻接矩阵O(V² + E)
邻接表O(V + E)
十字链表O(V + E)
邻接多重表O(V + E)

考研高频考点

  • 十字链表适用于有向图,邻接多重表适用于无向图(选择题高频易混淆)
  • 十字链表弧结点的五个域:tailvex、headvex、hlink、tlink、info(结构辨析题)
  • 四种存储结构的适用场景和优缺点对比(选择题/简答题高频考查)
  • 邻接多重表中每条边只有一个结点,删边只需删一个结点(对比邻接表)
  • 十字链表中求入度沿 firstin + hlink,求出度沿 firstout + tlink(概念题)
  • 邻接多重表 mark 标志位的作用(偶尔考)

易错:十字链表只适用于有向图。对应地,无向图有邻接多重表——每条边只存一个结点(邻接表中无向边要存两次)。408 需要区分两者的适用场景。

相关知识

  • 邻接表:十字链表是邻接表在有向图上的改进
  • 邻接矩阵:邻接矩阵判边 O(1) 但空间 O(V^2)
  • 拓扑排序:十字链表可以同时快速求入度和出度,方便拓扑排序

真题练习