第 5 讲 · 线性表与链表
线性表是有限序列 。它规定元素的逻辑次序,却没有规定元素必须怎样存。顺序表和链表是同一逻辑结构的两种实现。
线性表允许哪些操作
常见操作包括初始化、判空、求长度、按位置取值、按值查找、插入、删除、遍历、复制和销毁。选择实现时应先看最频繁的操作,而不是笼统地问哪一种结构“更好”。
顺序表
顺序表用数组保存元素。第 个元素可以 定位;若表有序,还能做 的折半查找。
在第 个位置插入,需要把原来第 到第 个元素依次后移,移动 个元素;删除第 个元素,需要把后面的 个元素前移。等概率位置下,平均都要移动约一半元素,因此是 。
移动方向很关键:插入时从后向前搬,避免覆盖尚未保存的元素;删除时从前向后搬。
单链表
单链表结点保存数据和后继指针:
typedef struct Node {
int data;
struct Node *next;
} Node;
头指针指向第一个结点;空表常用 NULL 表示。若引入不保存实际元素的头结点,很多首结点操作可以统一为普通结点操作。
遍历
for (Node *p = head; p != NULL; p = p->next) {
/* 访问 p->data */
}
循环不变量是:p 始终指向当前尚未处理的结点,p == NULL 表示链已走完。
在 p 后插入 q
q->next = p->next;
p->next = q;
第一句先让新结点接住原来的后半条链;第二句再让 p 指向新结点。顺序反过来会丢失原后继地址。
删除 p 后的结点
Node *q = p->next;
if (q != NULL) {
p->next = q->next;
free(q);
}
必须先保存被删结点,接好剩余链后再释放。
建表方式影响次序
- 头插法每次把新结点放在最前面,最终次序与输入相反;
- 尾插法维护尾指针,把新结点接到末尾,最终次序与输入相同。
尾插时空表需要同时更新头、尾;非空表更新原尾结点的 next,再移动尾指针。
有序链表合并
合并两条递增链表时,始终比较两条未处理链的首元素,把较小者接到结果尾部。循环不变量是:结果链已经有序,并且恰好包含两条输入链中已经取走的元素。
每个结点最多访问一次,时间 ,若复用原结点,额外空间 。
循环链表

循环单链表最后一个结点不指向 NULL,而指回首结点。它适合需要循环访问的问题,例如约瑟夫环。
遍历时不能再用 p != NULL 作为终止条件,而应保存起点,在回到起点时停止。空表、单结点自环尤其容易漏判。
双向链表
双向结点同时保存前驱和后继。已知结点时可向两个方向移动,删除当前结点也不必另找前驱,但插入或删除时要维护四条相关链接。
在结点 q 前插入 p:
p->prev = q->prev;
p->next = q;
q->prev->next = p;
q->prev = p;
应先把 p 的两条链接接好,再让原邻居改指向 p。
怎样选择
| 需求 | 顺序表 | 链表 |
|---|---|---|
| 按序号随机访问 | ||
| 已知位置后插入、删除 | 通常 | |
| 存储开销 | 无指针域 | 每结点有链接开销 |
| 内存布局 | 连续 | 可分散 |
| 容量变化 | 需扩容或预留 | 按需申请 |
链表所谓“插入删除快”有一个前提:位置已经找到。若先按值查找结点,整体仍可能是 。