【数据结构】链表变体:双向链表与循环链表

考点频率 :★★★★☆(选择题常考,重点考查双向链表与单链表的区别,以及循环链表的判断条件)

难度 :⭐⭐⭐

建议:重点掌握双向链表的节点结构及其在插入/删除时的指针修改特点,理解循环链表的遍历终止条件

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 本身的 prevnext 置空也算的话,就是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. 链表出现错误

🔔 本专栏日更,点击头像 → 专栏《软考中级高频考点》订阅,第一时间接收新内容

#软考中级 #软件设计师 #双向链表 #循环链表 #数据结构 #软考备考

相关推荐
evans在进步2 小时前
LeetCode 2 两数相加:链表模拟加法,Java 图解进位过程
java·leetcode·链表
yyy(十一月限定版)2 小时前
016【入门】双端队列
数据结构·c++
白狐_7982 小时前
408数据结构:中缀转后缀与操作符栈容量——真题精讲
前端·javascript·数据结构
iCxhust3 小时前
二进制文件编辑器ImHex安装步骤
笔记·微机原理·8088单板机
白狐_7983 小时前
408数据结构第5章:树与二叉树③——公式分类速查
数据结构·分类·数据挖掘
Iruoyaoxh3 小时前
数据结构排序
数据结构
来一碗刘肉面5 小时前
图的基本操作
数据结构
牛艺翔5 小时前
对一些基础知识的了解 笔记1
笔记
LccKyI6 小时前
C#学习day04
经验分享·笔记·学习·c#