2.3 双链表、循环链表与静态链表
双向指针带来的便利与代价、循环链表的判空条件、静态链表的游标机制,以及顺序表与链表的全面比较。
双链表
单链表只能从前往后遍历,要访问某结点的前驱只能从头开始,时间 。双链表在结点中增加一个指向前驱的指针 prior,访问前驱和后继都是 。
typedef struct DNode {
ElemType data;
struct DNode *prior, *next; // 前驱和后继指针
} DNode, *DLinkList;
插入与删除
双链表的按值查找、按位查找与单链表相同;但插入和删除要同时修改 prior 和 next 两个方向的指针,指针修改的顺序是选择题的高频考点。
动画演示双链表的插入与删除
p / 修改的指针新结点 s被删结点 q
C 代码
1// 在 p 结点之后插入 s 结点2bool InsertNextDNode(DNode *p, DNode *s) {3 s->next = p->next; // ①4 if (p->next != NULL)5 p->next->prior = s; // ②6 s->prior = p; // ③7 p->next = s; // ④8 return true;9}
1/6要在 p(20)之后插入新结点 s(25),需要修改 4 个指针
在 p 之前插入、删除 p 本身等操作,都可以借助 p->prior 转化为”在某结点之后操作”。
循环链表
循环单链表
表中最后一个结点的指针不是 NULL,而是指向头结点,整个链表形成一个环。
- 判空条件:
L->next == L(头结点的 next 指向自己)。 - 判断 p 是否为尾结点:
p->next == L。 - 从任意一个结点出发都能访问到链表中的每一个结点。
- 若只设尾指针 r,则头结点为
r->next,在表头、表尾插入都只需 ——因此循环单链表常常只设尾指针。
循环双链表
头结点的 prior 指向表尾结点,表尾结点的 next 指向头结点。
判空条件:L->next == L && L->prior == L。
静态链表
静态链表借助数组来描述线性表的链式存储结构:结点有数据域 data 和指针域 next,但这里的”指针”是结点在数组中的相对地址(数组下标),又称游标。
#define MaxSize 50
typedef struct {
ElemType data;
int next; // 下一个元素的数组下标
} SLinkList[MaxSize];
上图中,从头结点(下标 0)出发,next = 2,于是第一个元素是 data[2] = a;data[2] 的 next = 1,第二个元素是 data[1] = b……直到 next == -1 表示表尾。可见元素在数组中的物理顺序与逻辑顺序无关。
- 与顺序表一样需要预先分配一块连续的内存空间;
- 插入、删除只需修改游标,不需要移动元素;
- 不能随机存取,只能从头结点开始依次往后查找;
- 适用于不支持指针的高级语言(如早期的 Basic),或数据元素数量固定不变的场景(如操作系统的文件分配表 FAT)。
顺序表 vs 链表
| 比较维度 | 顺序表 | 链表 |
|---|---|---|
| 存取方式 | 随机存取 | 顺序存取 |
| 逻辑与物理结构 | 逻辑相邻 ⇒ 物理相邻 | 逻辑相邻不一定物理相邻,由指针表示 |
| 按位查找 | ||
| 按值查找(无序) | ||
| 按值查找(有序) | (折半查找) | |
| 插入/删除 | 平均移动半个表 | 修改指针 (需先找到位置) |
| 空间分配 | 预先分配,过大浪费、过小溢出 | 按需分配,灵活 |
| 存储密度 | 高(= 1) | 低(指针占用额外空间) |
怎样选择?
- 难以估计线性表的长度或存储规模时,用链表;
- 经常做按序号访问、很少插入删除时,用顺序表;
- 频繁插入删除时,用链表;
- 顺序表基于数组,实现简单、任何语言都支持;链表基于指针。
即时练习
即时练习5 题
点击选项即可作答
- 1在双链表中,在 p 所指结点之后插入 s 所指结点,正确的语句序列是( )。
- 2某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用( )存储方式最省时间。
- 3与单链表相比,双链表的优点之一是( )。
- 4静态链表中指针表示的是( )。
- 5已知头指针 h 指向一个带头结点的非空单循环链表,结点结构为 (data, next),其中 next 是指向直接后继结点的指针,p 是尾指针,q 是临时指针。现要删除该链表的第一个元素,正确的语句序列是( )。
学完了吗?
标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。