双链表、循环链表与静态链表
这一节的统考题全是指针语句题:2016 双链表删除、2021 循环单链表删除首元素、2023 双链表插入。 另一类高频题是「给一组操作,选最省时的链表」,靠的是同一条推理:每个操作要用到哪个结点,这个结点能不能
机制
2.3.3 双链表
结点有两个指针 prior 和 next,分别指向直接前驱和直接后继。表头结点的 prior 和尾结点的 next 都是 NULL。
- 按值查找、按位查找与单链表相同。
- 插入、删除要多改
prior,关键是保证修改过程中不断链。 - 可以直接找到前驱,所以插入、删除本身都是
。
在 *p 之后插入 *s(2023 命题追踪):
s->next=p->next; // ①
p->next->prior=s; // ②
s->prior=p; // ③
p->next=s; // ④语句顺序不唯一,但 ① 必须在 ④ 之前,否则 *p 的后继指针就丢了。
删除 *p 的后继 *q(2016 命题追踪):
p->next=q->next; // ①
q->next->prior=p; // ②
free(q);在双链表的两个结点之间插入一个新结点,要改 4 个指针域:新结点的 prior、next,前一个结点的 next,后一个结点的 prior。
2.3.4 循环链表
循环单链表:尾结点的 next 不是 NULL,而是指向头结点,整个链表形成一个环。
- 判空条件是
L->next==L,不是头结点的指针是否为空。带头结点的循环单链表中没有空指针。 - 在任何位置上的插入和删除都是等价的,无须判断是否是表尾。
- 可以从任意一个结点开始遍历整个链表。
- 常只设尾指针
r,不设头指针:r->next就是头结点,在表头和表尾插入都只需;只设头指针时,在表尾插入要 。
循环双链表:头结点的 prior 还要指向表尾结点。*p 为尾结点时 p->next==L;空表时头结点的 prior 和 next 都等于 L。
2.3.5 静态链表
用数组描述线性表的链式存储结构。结点也有 data 和 next,但 next 是结点在数组中的相对地址(数组下标),也称游标。
#define MaxSize 50
typedef struct{
ElemType data;
int next; // 下一个元素的数组下标
}SLinkList[MaxSize];- 和顺序表一样,要预先分配一块连续的内存空间,最大容量在定义时就确定了。
- 以
next==-1作为结束的标志。 - 插入、删除与动态链表相同,只需修改指针,不需要移动元素。
- 适用于不支持指针的高级语言(如 Basic)。
2.3.6 顺序表和链表的比较
四个维度(存取方式、逻辑结构与物理结构、查找插入删除、空间分配)和选用原则收在 速查:顺序表与链表。一句话:通常较稳定的线性表选择顺序存储,频繁插入、删除的线性表宜选择链式存储。
手算模板
「选最省时的链表」:把每个操作要找的结点列出来,看哪种结构能
| 操作 | 需要 |
|---|---|
| 在表头插入 / 删除第一个元素 | 头结点(或首元结点的前驱) |
| 在表尾插入 | 尾结点 |
| 删除尾结点 | 尾结点的前驱——只有双链表能 |
| 在循环单链表中删除首元结点 | 首元的前驱:有头结点时是头结点;无头结点时是尾结点 |
王道 2.3.7 的五道同类题,答案都能用这张表推出来:
| 题 | 操作组合 | 答案 | 理由 |
|---|---|---|---|
| 第 23 题 | 末尾插入、删除(末尾)结点 | 带头结点的循环双链表 | 要找尾结点及其前驱 |
| 第 24 题 | 删首、删尾、首前插、尾后插 | 只有头结点指针的循环双链表 | L->prior 即尾结点,尾结点的 prior 即其前驱,四个操作都是 |
| 第 29 题 | 在最后插入、删除第一个 | 不带头结点且有尾指针的循环单链表 | r->next 是首元结点,两个操作都是 |
| 第 25 题 | 两个指针都指向各自的尾结点 | 未指明谁接在谁后面,要同时 | |
| 第 26 题 | 删除首元结点却要 | 只有表头指针、没有头结点 | 首元的前驱是尾结点,只能遍历去找 |
边界
| 说法 | 判断 | 说明 |
|---|---|---|
| 「双链表的优点是插入、删除更方便」 | ❌ | 两者插入删除都不用移动元素,双链表改指针反而更复杂。优点是访问前后相邻结点更灵活 |
| 「双链表可以随机访问」 | ❌ | 仍是顺序存取 |
| 「双链表插入的四句可以任意排列」 | ❌ | 顺序不唯一但不任意:① s->next=p->next 必须在 ④ p->next=s 之前 |
| 「在双链表两个结点之间插入要改 2 个指针」 | ❌ | 改 4 个 |
| 「带头结点的循环单链表判空:头结点的指针域为空」 | ❌ | 判空是 L->next==L,即头结点的指针域与 L 的值相等,不是与 L 的地址相等 |
「循环双链表判空:L->prior==NULL && L->next==NULL」 | ❌ | 是 L->prior==L && L->next==L |
「head->next->next==head 说明表长为 1」 | ❌ | 表长为 0 或 1 都成立 |
| 「非循环双链表删除尾结点是 | ❌ | 没有尾指针,要走到表尾, |
| 「静态链表存取第 | ❌ | 空间是顺序分配的,但元素按游标依次查找,与 |
| 「静态链表的最大容量可以增加」 | ❌ | 在定义时就确定,以后不能增加 |
| 「静态链表插入删除要移动元素」 | ❌ | 只改游标 |
| 「顺序存储只能存线性结构」 | ❌ | 也可以存储图和树(如完全二叉树的顺序存储) |
错题复盘:2016 删除循环双链表中的
*p正确答案 D:
p->next->prev=p->prev; p->prev->next=p->next; free(p);两句各自把*p的一侧邻居绕过*p接到另一侧邻居上。口诀:后继的前驱 = 我的前驱;前驱的后继 = 我的后继。 选项 A 第二句写成p->prev->next=p->prev,前驱结点指向它自己;选项 B、C 第一句写成p->next->prev=p->next,后继结点指向它自己。
错题复盘:2021 删除循环单链表的第一个元素
带头结点的非空循环单链表,
h为头指针,p为尾指针。答案 D:q=h->next; h->next=q->next; if(p==q) p=h; free(q);难点是if(p==q) p=h;:表中只有一个元素时,被删的首元结点同时就是尾结点,删掉后尾指针必须改指向头结点,否则p指向已释放的结点。选项 C 把条件写反成p!=q。 已用模拟跑过:D 在「多个元素」和「只有一个元素」两种情况下都正确;选项 B 漏了这一句,只在只有一个元素时出错。
错题复盘:2023 双链表插入,已执行前两句
已执行
s->next=p->next; p->next=s;,还需执行的是 C:s->prev=s->next->prev; s->next->prev=s;执行完前两句后,p->next已经是s,不再是原后继。选项 B 的p->next->prev=s实际是s->prev=s。原后继只能通过s->next找到,而它的prev此时还指向p,所以s->prev=s->next->prev正好把p赋给s->prev。 做这类题先画出前两句执行后的状态,再逐个检查选项里的p->next实际指向谁。
错题复盘:在
*p之前插入*q(2.3.7 第 15、17 题)第 15 题答案:
p->prior->next=q; q->next=p; q->prior=p->prior; p->prior=q;。p->prior必须最后改,因为前三句都要通过它找到原前驱。 第 17 题(在、 之间插入 , p指向, q指向)的约束是: q->next=p->next和p->next->prior=q都要在p->next=q之前,否则p->next已变成,找不到 。
考点
- 双链表插入、删除的语句顺序:2016、2023。
- 循环单链表删除首元时维护尾指针:2021。
- 判空条件:单链表 / 循环单链表 / 循环双链表各不相同。
- 「选最省时的链表」:看删除尾结点需不需要前驱。
- 静态链表:数组实现、游标、
next==-1、不移动元素、容量固定。
链接
- 🏠 返回总览:数据结构第 1~4 章总览
- ⬅️ 上一节:2.3.1~2.3.2 单链表
- ➡️ 下一章:3.1.1 出栈序列:合法性判定与卡特兰数计数
- 🔗 四种链表对照、2.3.6 四个维度:速查:顺序表与链表
- 🔗 链式队列就是带头尾指针的单链表:速查:栈与队列的判空判满
- 📖 名词库:第 1~4 章名词库