Skip to content

408

数据结构

DS-02-03 线性表的链式存储(单链表/双链表/循环链表)


一、定位信息

项目内容
所属圈层核心层
考点热度H级(高频重点) — 链表是408数据结构的核心考点,近5年大题几乎每年都有链表算法设计题,分值6–12分
前置知识回顾需掌握线性表ADT(DS-02-01)、顺序表操作(DS-02-02)、指针与动态内存分配的基本概念
知识网络定位链式存储是线性表的第二种物理实现,与顺序存储互补。单链表是栈/队列链式实现的基础,双链表是操作系统内存管理的基础

二、知识点讲解

2.1 链式存储的基本思想

链式存储不要求元素在内存中连续存放,而是通过指针将分散的存储单元串联起来,形成一条"链"。

每个结点分为两部分:

  • 数据域:存储数据元素
  • 指针域:存储下一个结点的地址

直观理解:链表就像寻宝游戏——每个宝箱里有宝物(数据)和一张纸条,纸条写着下一个宝箱的位置。你只能按纸条的指引一个一个找,不能直接跳到第 ii 个宝箱。

2.2 单链表(Singly Linked List)

存储结构定义
c
typedef struct LNode {       // 单链表结点类型
    ElemType data;           // 数据域
    struct LNode *next;      // 指针域,指向下一个结点
} LNode, *LinkList;          // LNode是结点类型,LinkList是指向结点的指针类型

关键概念

  • 头指针:指向链表第一个结点的指针,是链表的"入口"
  • 头结点:在第一个数据结点之前附加的一个结点(数据域可空),头结点不计入表长
  • 带头结点 vs 不带头结点:408考试中通常默认带头结点,因为带头结点可以统一处理空表和非空表的操作

【图示说明】带头结点的单链表:HEAD → [头结点] → [a₁] → [a₂] → ... → [aₙ] → NULL

  • HEAD 指向头结点
  • 头结点的 next 指向第一个数据结点 a₁
  • 最后一个结点 aₙ 的 next 指向 NULL
基本操作

(1)初始化 InitList

c
// 初始化一个带头结点的空单链表
bool InitList(LinkList &L) {
    L = (LNode *)malloc(sizeof(LNode));  // ① 创建头结点
    if (L == NULL)                        // ② 内存分配失败
        return false;
    L->next = NULL;                       // ③ 头结点的next置空
    return true;
}

时间复杂度:O(1)O(1)

(2)头插法建表(重要!)

c
// 头插法:将数组a的n个元素逆序建立单链表
LinkList CreateList_Head(int a[], int n) {
    LinkList L;
    InitList(L);                              // ① 初始化头结点
    for (int i = 0; i < n; i++) {             // ② 逐个插入
        LNode *s = (LNode *)malloc(sizeof(LNode)); // ③ 创建新结点
        s->data = a[i];                       // ④ 赋值
        s->next = L->next;                    // ⑤ 新结点指向原第一个结点
        L->next = s;                          // ⑥ 头结点指向新结点
    }
    return L;
}
  • 特点:插入顺序与输入顺序相反(逆序建表)
  • 时间复杂度:O(n)O(n)

(3)尾插法建表(重要!)

c
// 尾插法:将数组a的n个元素顺序建立单链表
LinkList CreateList_Tail(int a[], int n) {
    LinkList L;
    InitList(L);                              // ① 初始化头结点
    LNode *r = L;                             // ② r指向尾结点(初始为头结点)
    for (int i = 0; i < n; i++) {             // ③ 逐个插入
        LNode *s = (LNode *)malloc(sizeof(LNode)); // ④ 创建新结点
        s->data = a[i];                       // ⑤ 赋值
        r->next = s;                          // ⑥ 尾结点指向新结点
        r = s;                                // ⑦ 更新尾指针
    }
    r->next = NULL;                           // ⑧ 尾结点的next置空
    return L;
}
  • 特点:插入顺序与输入顺序相同(顺序建表)
  • 时间复杂度:O(n)O(n)

(4)按位查找 GetElem

c
// 查找第i个位置的结点(带头结点),返回结点指针
LNode *GetElem(LinkList L, int i) {
    if (i < 0)                           // ① i<0不合法
        return NULL;
    LNode *p = L;                        // ② p从头结点开始
    int j = 0;                           // ③ j记录当前位序(头结点位序为0)
    while (p != NULL && j < i) {         // ④ 循环找第i个结点
        p = p->next;                     // ⑤ p后移
        j++;                             // ⑥ 位序加1
    }
    return p;                            // ⑦ 返回第i个结点的指针(或NULL)
}
  • 时间复杂度O(n)O(n)(最坏需遍历整个链表)
  • 注意:i=0 时返回头结点,i=1 时返回第一个数据结点

(5)插入操作 ListInsert(后插操作)

c
// 在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e) {
    LNode *p = GetElem(L, i - 1);       // ① 找到第i-1个结点(前驱)
    if (p == NULL)                       // ② 前驱不存在,插入位置不合法
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode)); // ③ 创建新结点
    s->data = e;                         // ④ 赋值
    s->next = p->next;                   // ⑤ 新结点指向原第i个结点
    p->next = s;                         // ⑥ 前驱指向新结点
    return true;
}
  • 时间复杂度O(n)O(n)(主要耗时在查找前驱结点)
  • 核心操作只有两步(⑤和⑥),顺序不能颠倒!

(6)删除操作 ListDelete

c
// 删除第i个位置的元素,用e返回(带头结点)
bool ListDelete(LinkList &L, int i, ElemType &e) {
    LNode *p = GetElem(L, i - 1);       // ① 找到第i-1个结点(前驱)
    if (p == NULL || p->next == NULL)    // ② 前驱不存在或第i个结点不存在
        return false;
    LNode *q = p->next;                  // ③ q指向要删除的结点
    e = q->data;                         // ④ 保存被删元素的值
    p->next = q->next;                   // ⑤ 前驱指向后继
    free(q);                             // ⑥ 释放被删结点
    return true;
}
  • 时间复杂度O(n)O(n)

2.3 双链表(Doubly Linked List)

存储结构定义
c
typedef struct DNode {       // 双链表结点类型
    ElemType data;           // 数据域
    struct DNode *prior;     // 前驱指针
    struct DNode *next;      // 后继指针
} DNode, *DLinkList;

优势:可以双向遍历,删除给定结点时无需查找前驱(O(1)O(1))。

插入操作(在结点p之后插入结点s)
c
// 在双链表的结点p之后插入结点s(4步,顺序可调整但需保证不丢失指针)
s->next = p->next;        // ① s的后继指向p的后继
p->next->prior = s;       // ② p的后继的前驱指向s(前提:p->next != NULL)
s->prior = p;             // ③ s的前驱指向p
p->next = s;              // ④ p的后继指向s

时间复杂度O(1)O(1)(已知结点p的情况下)

删除操作(删除结点p之后的结点q)
c
// 删除双链表中结点p的后继结点q
q = p->next;              // ① q指向要删除的结点
p->next = q->next;        // ② p的后继指向q的后继
if (q->next != NULL)      // ③ 若q不是最后一个结点
    q->next->prior = p;   // ④ q的后继的前驱指向p
free(q);                  // ⑤ 释放q

时间复杂度O(1)O(1)

2.4 循环链表(Circular Linked List)

循环单链表:最后一个结点的 next 不指向 NULL,而是指向头结点(或第一个结点)。

循环双链表:头结点的 prior 指向尾结点,尾结点的 next 指向头结点。

判断空表

  • 循环单链表(带头结点):L->next == L
  • 循环双链表(带头结点):L->next == L && L->prior == L

优势:从任意结点出发可以遍历整个链表;在尾部操作时若已知尾结点,可以 O(1)O(1) 完成。

2.5 三种链表综合对比

对比项单链表双链表循环链表
指针域1个(next)2个(prior, next)1或2个
空间开销最小最大(每个结点多一个指针)同单/双链表
删除已知结点p需找前驱 O(n)O(n)不需找前驱 O(1)O(1)同单/双链表
双向遍历取决于单/双
判空条件L->next == NULL双重判断L->next == L
适用场景一般场景需要频繁双向遍历约瑟夫环等环形问题

三、记忆与理解辅助

技巧1:头插法 vs 尾插法口诀

"头插逆序尾插顺,头插简单尾要针"

  • 头插法:结果与输入逆序,实现简单(只需头指针)
  • 尾插法:结果与输入顺序,需要额外的尾指针 r

技巧2:单链表插入/删除的"绑线"口诀

"先绑新线再断旧,顺序反了就断链"

插入时:

  1. 先让新结点 s 接上后面的结点(s->next = p->next
  2. 再让前驱 p 指向新结点(p->next = s

如果顺序反了,p->next 的原值就丢失了!

技巧3:双链表插入的4步记忆法

"先连后继再连前,两前两后四步全"

s->next = p->next;        // 连后:s连上p的后继
p->next->prior = s;       // 反连:p的后继连回s
s->prior = p;             // 连前:s连上p
p->next = s;              // 反连:p连上s

技巧4:链表类型选择速查表

需求推荐类型
只需单向遍历单链表
需要频繁双向遍历双链表
删除已知结点(不给前驱)双链表(O(1)O(1)
环形结构(约瑟夫环等)循环链表
内存紧张单链表(指针域最少)

四、例题与精解

例题1(基础)

命题意图:考查单链表头插法和尾插法的区别。

题目:依次将数据 (1, 2, 3, 4, 5) 插入带头结点的空单链表中。分别写出用头插法和尾插法建立的链表的数据序列。

审题分析

  • 输入序列:1, 2, 3, 4, 5
  • 需要分别用头插法和尾插法模拟建表过程

解题思路:头插法每次插在头结点之后(最前面),尾插法每次插在最后面。

完整步骤

头插法过程

步骤插入元素链表状态(头结点后的数据序列)
111
222 → 1
333 → 2 → 1
444 → 3 → 2 → 1
555 → 4 → 3 → 2 → 1

尾插法过程

步骤插入元素链表状态(头结点后的数据序列)
111
221 → 2
331 → 2 → 3
441 → 2 → 3 → 4
551 → 2 → 3 → 4 → 5

结果

  • 头插法:5 → 4 → 3 → 2 → 1逆序
  • 尾插法:1 → 2 → 3 → 4 → 5顺序

方法反思:在408算法大题中,若要求"按输入顺序建立链表",必须用尾插法;若要求"逆序",用头插法更简洁。审题时务必注意这一点。


例题2(中等)

命题意图:考查单链表的算法设计能力(就地逆置)。

题目:设计一个算法,将带头结点的单链表就地逆置(即不额外申请新结点),要求时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)

审题分析

  • 输入:带头结点的单链表 LL
  • 输出:逆置后的 LL(原地修改)
  • 约束:不能新建结点,只能修改指针

解题思路:用头插法逆置思想——依次摘下每个数据结点,用头插法重新插入到头结点之后。这样原来在后面的结点会被插到前面,实现逆置。

完整步骤

c
// 将带头结点的单链表就地逆置
void ReverseList(LinkList &L) {
    LNode *p = L->next;      // ① p指向第一个数据结点
    L->next = NULL;          // ② 断开头结点与后续结点的连接
    
    while (p != NULL) {      // ③ 遍历原链表的每个结点
        LNode *q = p->next;  // ④ 保存p的后继(因为马上要修改p->next)
        p->next = L->next;   // ⑤ 头插法步骤1:p指向原第一个结点
        L->next = p;         // ⑥ 头插法步骤2:头结点指向p
        p = q;               // ⑦ p移动到下一个待处理结点
    }
}

图示过程(以 L=(1,2,3)L = (1, 2, 3) 为例):

步骤p链表状态(头结点后)说明
初始11 → 2 → 3p指向1
1断开头结点
第1轮⑤⑥21将1头插
第2轮⑤⑥32 → 1将2头插
第3轮⑤⑥NULL3 → 2 → 1将3头插

结果L=(3,2,1)L = (3, 2, 1)

复杂度分析

  • 时间:每个结点访问一次,T(n)=O(n)T(n) = O(n)
  • 空间:只用常数个指针变量,S(n)=O(1)S(n) = O(1)

方法反思

  • 核心思想:头插法天然产生逆序,利用这个特性实现逆置
  • 步骤④保存后继是关键,不保存的话步骤⑤会丢失后续结点
  • 变式:可以用"三指针法"(pre, cur, next)从前往后逐个反转指针方向,效果相同

五、考情分析

分析维度说明
考查频次近5年每年必考,是408数据结构出题最密集的知识点之一
常见题型选择题(链表操作分析、头插法/尾插法辨析);大题(链表算法设计,如逆置、合并、查找倒数第k个等)
分值占比约6–12分(大题8分居多,选择题2分)
命题趋势链表大题难度稳定在中等偏上,常结合双指针、递归等技巧;近年出现过"链表+排序"、"链表+环检测"等综合题

六、易错点提醒

易错点1

  • 错误表现:单链表插入/删除时,先修改 p->next 再保存原值
  • 错误原因:没有意识到修改 p->next 会丢失原链表信息
  • 正确做法:插入时先 s->next = p->next(先让新结点接上后面),再 p->next = s。删除时先 q = p->next 保存要删的结点,再 p->next = q->next

易错点2

  • 错误表现:混淆"带头结点"和"不带头结点"的链表操作差异
  • 错误原因:头结点的存在使得所有操作(包括第一个结点的操作)都可以统一处理
  • 正确做法:408考试中默认带头结点。带头结点时,LL 永远不为NULL(空表时 Lnext=NULLL\rightarrow next = NULL);不带头结点时,空表 L=NULLL = NULL

易错点3

  • 错误表现:双链表插入时遗漏某一步指针修改,导致链表断裂或指针悬空
  • 错误原因:双链表有4步操作,容易漏掉某一步
  • 正确做法:牢记"两前两后"——每个方向的连接都要做两次(spps),共4步

易错点4

  • 错误表现:尾插法建表后忘记将尾结点的 next 置为 NULL
  • 错误原因malloc 分配的内存内容不确定,不置空可能导致野指针
  • 正确做法:尾插法最后必须 r->next = NULL,头插法因为是从头结点的 next 开始,头结点初始化时已置 NULL,所以不用额外处理

易错点5

  • 错误表现:在链表算法中不考虑空表或只有一个结点的边界情况
  • 错误原因:只关注一般情况,忽略特殊情况
  • 正确做法:算法题中务必检查:空表(Lnext=NULLL\rightarrow next = NULL)、单结点表、头结点/尾结点等边界情况

七、来源标注

  • 依据2026考研统考大纲——数据结构部分"线性表的链式存储"
  • 依据《数据结构(C语言版)》严蔚敏版第二章
  • 依据《数据结构》王道考研辅导讲义

考研全科复习资料 - 基于2026考研统考大纲