考点频率 :★★★★☆(选择题常考,重点考查双向链表与单链表的区别,以及循环链表的判断条件)
难度 :⭐⭐⭐
建议:重点掌握双向链表的节点结构及其在插入/删除时的指针修改特点,理解循环链表的遍历终止条件
1️⃣ 为什么需要链表变体?
上一篇我们讲了单链表。单链表的结构简单,但有一个明显的缺点:只能单向移动。你只能从前往后走,不能从后往前走。如果我想删除某个节点,必须知道它的前驱节点是谁------这在单链表中需要从头遍历才能找到。
就像一条单行道,你只能往前开,不能掉头。想回去?得绕一大圈从头再来。
于是,人们发明了两种变体来解决这个问题:
- 双向链表:给每个节点增加一个指向前驱的指针,实现双向移动
- 循环链表:把尾节点的指针指向头节点,实现循环遍历
2️⃣ 双向链表(Doubly Linked List)
2.1 节点结构
双向链表的每个节点包含三个部分:
| 组成部分 | 含义 |
|---|---|
| 前驱指针(prev) | 指向前一个节点的地址 |
| 数据域(data) | 存储数据 |
| 后继指针(next) | 指向后一个节点的地址 |
c
// 双向链表节点的结构定义
typedef struct DNode {
int data; // 数据域
struct DNode* prev; // 前驱指针
struct DNode* next; // 后继指针
} DNode;
与单链表的区别 :单链表只有
next指针,双向链表多了prev指针,可以向前和向后双向遍历。
2.2 双向链表的核心特点
| 特点 | 说明 |
|---|---|
| 双向遍历 | 可以从头到尾,也可以从尾到头 |
| 删除更灵活 | 删除节点时,如果已知该节点的指针,无需遍历找前驱,直接通过 prev 就能找到 |
| 插入更灵活 | 可以在节点之前插入,也可以在节点之后插入 |
| 空间开销更大 | 每个节点多存储一个指针,占用更多内存 |
2.3 双向链表的插入操作
以在节点 p 之后插入新节点 s 为例:
c
// 在节点 p 之后插入新节点 s
s->next = p->next; // 新节点的 next 指向 p 的后继
s->prev = p; // 新节点的 prev 指向 p
if (p->next != NULL) {
p->next->prev = s; // p 的后继的 prev 指向 s
}
p->next = s; // p 的 next 指向 s
时间复杂度 :O(1)O(1)O(1)(前提是已知插入位置的前驱节点)
2.4 双向链表的删除操作
以删除节点 q 为例:
c
// 删除节点 q(假设 q 不为 NULL)
q->prev->next = q->next; // q 的前驱的 next 指向 q 的后继
if (q->next != NULL) {
q->next->prev = q->prev; // q 的后继的 prev 指向 q 的前驱
}
free(q); // 释放 q 的内存
时间复杂度 :O(1)O(1)O(1)(前提是已知待删除节点的指针)
单链表 vs 双向链表的删除 :在单链表中,如果只知道要删除的节点本身,无法直接删除它(因为找不到它的前驱),必须从头遍历。双向链表通过
prev指针直接找到前驱,无需遍历。
3️⃣ 循环链表(Circular Linked List)
3.1 什么是循环链表?
循环链表是一种首尾相连 的链表。在单链表或双向链表的基础上,将最后一个节点的 next 指针指向头节点(或头指针),形成一个环。
打个比方 :单链表像一条有终点的单向公路 ,走到头就是
NULL(死路)。循环链表像一条环形跑道------你从起点出发,绕了一圈还能回到起点,永远不会遇到死路。
3.2 循环链表的两种形式
| 类型 | 结构特点 | 遍历终止条件 |
|---|---|---|
| 单循环链表 | 尾节点的 next 指向头节点 |
回到头节点时停止 |
| 双循环链表 | 头节点的 prev 指向尾节点,尾节点的 next 指向头节点 |
回到头节点时停止 |
3.3 循环链表的遍历
遍历终止条件的判断方式是唯一的考点。
- 非循环链表 :
p == NULL时停止 - 循环链表 :
p == head时停止(已经绕完一圈回到起点)
c
// 循环单链表的遍历
Node* p = head->next; // 从第一个数据节点开始(带头节点)
while (p != head) { // 回到头节点就停止
printf("%d ", p->data);
p = p->next;
}
3.4 循环链表的适用场景
| 应用场景 | 原因 |
|---|---|
| 约瑟夫环问题 | 循环删除节点,利用环形结构自然实现 |
| 操作系统进程调度(时间片轮转) | 循环遍历就绪队列,每个进程轮流获得CPU时间 |
| 环形缓冲区 | 数据循环读写,不需要移动数据 |
4️⃣ 三种链表完整对比表(重点)
| 对比项 | 单链表 | 双向链表 | 循环链表(以单循环为例) |
|---|---|---|---|
| 节点结构 | data + next | data + prev + next | data + next |
| 遍历方向 | 只能向前 | 可前可后 | 只能向前,但可循环 |
| 遍历终止条件 | p == NULL |
p == NULL |
p == head |
| 空间开销 | 小(1个指针/节点) | 大(2个指针/节点) | 小(1个指针/节点) |
| 查找前驱 | 需从头遍历 O(n)O(n)O(n) | 直接通过 prev O(1)O(1)O(1) |
需从头遍历 O(n)O(n)O(n) |
| 删除已知节点 | O(n)O(n)O(n)(需找前驱) | O(1)O(1)O(1)(直接通过 prev) |
O(n)O(n)O(n)(需找前驱) |
| 典型应用 | 简单线性结构 | 需要反向遍历的场景 | 环形队列、约瑟夫环 |
5️⃣ 经典例题
例题1:在双向链表中,删除一个已知节点(非头节点、非尾节点),需要修改的指针数量是( )。
A. 2 个
B. 3 个
C. 4 个
D. 5 个
解析 :删除已知节点 q 需要修改:q->prev->next(1个)、q->next->prev(1个),共修改 2 个指针 。但如果把 q 本身的 prev 和 next 置空也算的话,就是4个。软考中通常问的是"修改链表中的指针",答案是 2 个 。选 A。
例题2:在带头节点的单向循环链表中,判断链表为空的条件是( )。
A. head == NULL
B. head->next == NULL
C. head->next == head
D. head == head->next
解析 :带头节点的单向循环链表中,空链表的头节点的 next 指向它自己。选 C。
例题3(概念判断):双向链表比单链表占用更多的存储空间,但删除操作更灵活。( )
解析 :正确。双向链表每个节点多存储一个 prev 指针,空间开销更大;但删除已知节点时无需遍历找前驱,更加灵活。
6️⃣ 记忆口诀
双向链表两个针,前后遍历都可行。
删除不用找前驱,直接通过 prev 拎。
循环链表首尾连,遍历到头不算完。
判断空表看头尾,
head->next == head是关键。
7️⃣ 小测验(评论区对答案)
在带头节点的双向循环链表中,若
head->next == head,则表示( )。A. 链表中有 1 个数据节点
B. 链表为空
C. 链表已满
D. 链表出现错误
🔔 本专栏日更,点击头像 → 专栏《软考中级高频考点》订阅,第一时间接收新内容
#软考中级 #软件设计师 #双向链表 #循环链表 #数据结构 #软考备考