第 2 章 线性表(顺序表与链表)
本章地位:线性表是 408 数据结构大题的第一主战场:2009(找链表倒数第 \(k\) 个结点)、2010(数组循环左移)、2012(两链表公共后缀起点)等真题大题全部以本章为背景,选择题更是密集轰炸头结点辨析、插入删除移动次数、随机存取与顺序存取、双指针技巧。学习目标:顺序表 / 单链表 / 双链表 / 循环链表 / 静态链表的概念全过关,基本操作能默写 C 代码并分析复杂度,形成「双指针」「对折」「归并」三大算法设计套路。
2.1 线性表的定义与基本操作
2.1.1 定义:有限序列与位序
2.1.2 基本操作与「逻辑 vs 实现」
判断正误:(1) 线性表中每个元素都有且仅有一个直接前驱和一个直接后继;(2) 线性表的长度是固定不变的;(3) 同一种逻辑结构可以用不同的存储结构实现;(4) 链表一定比顺序表节省空间。
查看答案
(1) 错:端点例外——表头无前驱、表尾无后继,须说「除端点外」。(2) 错:表长随插入删除动态变化;顺序表静态分配时固定的是「容量」不是「长度」。
(3) 对:线性表既可顺序存储(顺序表)也可链式存储(链表)。(4) 错:链表每个结点要额外存指针(存储密度小于 1),存同样多元素并不省空间;它省的是「不需要一整块连续空间、容量可按需增长」(见 2.4 对比表)。
2.2 顺序表 高频考点
2.2.1 地址计算与随机存取
// 顺序表类型定义:静态分配(动态分配则用 int *data 指针 + malloc,容量可扩)
#define MaxSize 50 // 静态分配:容量在编译期确定
typedef struct {
int data[MaxSize]; // 存元素的数组
int length; // 当前表长
} SqList;数值例:\(\mathrm{int}\) 型 \(c=4\),\(\mathrm{LOC}(A)=2000\):下标 4(位序 5)的地址 \(=2000+4\times4=2016\);地址 2024 处的下标 \(=(2024-2000)/4=6\) ✓。
2.2.2 插入 / 删除的移动次数推导
- 插入到位序 \(i\)(\(1\le i\le n+1\)):\(a_i\cdots a_n\) 共 \(n-i+1\) 个元素后移一位(先挪尾部再腾位);
- 删除位序 \(i\)(\(1\le i\le n\)):\(a_{i+1}\cdots a_n\) 共 \(n-i\) 个元素前移一位。
表长 \(n=5\) 的顺序表:(1) 分别求位序 \(i=1,2,3,4,5,6\) 插入时移动的元素个数并验证平均 \(\frac{n}{2}\);(2) 位序 \(i=1,\dots,5\) 删除时的移动次数并验证平均 \(\frac{n-1}{2}\)。
查看解答
| 位序 \(i\) | 1 | 2 | 3 | 4 | 5 | 6 | 合计 / 平均 |
|---|---|---|---|---|---|---|---|
| 插入移动 \(n-i+1\) | 5 | 4 | 3 | 2 | 1 | 0 | \(15\),\(\frac{15}{6}=2.5=\frac{n}{2}\) ✓ |
| 删除移动 \(n-i\) | 4 | 3 | 2 | 1 | 0 | — | \(10\),\(\frac{10}{5}=2=\frac{n-1}{2}\) ✓ |
快速验算技巧:插入最多动 \(n\) 个(插表头)、最少动 0 个(插表尾后);删除最多动 \(n-1\) 个(删表头)、删表尾动 0 个。选项中出现「\(\frac{n+1}{2}\)」「\(\frac{n}{2}-1\)」等都是干扰项。
2.2.3 三个基本操作的 C 实现
// 位序 i 从 1 开始,数组下标从 0 开始;bool 需要 stdbool.h
bool ListInsert(SqList *L, int i, int e) {
if (i < 1 || i > L->length + 1) return false; // 位序越界
if (L->length >= MaxSize) return false; // 表满(静态分配)
for (int j = L->length; j >= i; j--) // 从最后一个开始后移
L->data[j] = L->data[j - 1];
L->data[i - 1] = e; // 位序 i 落在下标 i-1
L->length++;
return true;
}
bool ListDelete(SqList *L, int i, int *e) {
if (i < 1 || i > L->length) return false;
*e = L->data[i - 1];
for (int j = i; j < L->length; j++) // 从前往后挪,补上空位
L->data[j - 1] = L->data[j];
L->length--;
return true;
}
int LocateElem(SqList L, int e) { // 按值查找,返回位序
for (int j = 0; j < L.length; j++)
if (L.data[j] == e) return j + 1; // 下标 j ↔ 位序 j+1
return 0; // 未找到
} 查看解答
复杂度:插入 / 删除的主要开销是移动循环,最坏 \(O(n)\)、等概率 \(O(n)\);按位取 \(\mathrm{GetElem}\) 是 \(O(1)\)(随机存取);按值查找 \(O(n)\)(无序表只能逐个比)。
数值验证(\(n=5\),插到位序 3):循环 \(j=5,4,3\) 共 3 次 \(=n-i+1=5-3+1=3\) ✓;删除位序 2:\(j\) 取 \(2,3,4\) 共 3 次 \(=n-i=5-2=3\) ✓(\(j=5\) 不满足 \(j<\mathrm{length}\),别误数成 4 次),被前移的正是 \(a_3,a_4,a_5\)——「数循环次数」必须代小 \(n\) 逐一核对,别背错。
易错:插入的合法范围是 \(1\le i\le n+1\)(可以插成新表尾),删除是 \(1\le i\le n\);移动方向相反——插入「从尾往前」挪,删除「从头往后」挪。
顺序表首地址 \(\mathrm{LOC}(A)=2000\),每个元素占 4 个单元:(1) 位序 1 与位序 7 的元素地址各是多少?(2) 地址 2040 处存的是位序几的元素?(3) 若元素改为每个占 8 单位,位序 6 的地址是多少?
查看答案
(1) 位序 1 即下标 0:\(2000\);位序 7 即下标 6:\(2000+6\times4=2024\)。
(2) 下标 \(=(2040-2000)/4=10\),位序 11。注意 2040 对应的是「第 11 个元素」,别答 10。
(3) 下标 5:\(2000+5\times8=2040\)。同一公式换 \(c\) 即可——单位换成 8 后「位序 6」与 (2) 中「位序 11」地址恰相同,说明地址由「下标 × 单元素大小」唯一决定。
2.3 链表 高频考点
2.3.1 单链表:结点与头结点
// 单链表结点类型:LNode 是结点,LinkList 是指向结点的指针(整个链表用头指针代表)
typedef struct LNode {
int data; // 数据域
struct LNode *next; // 指针域:指向后继结点,表尾结点为 NULL
} LNode, *LinkList;L->next == NULL(头指针 L 永不为 NULL),不带头结点则是 L == NULL;② 统一首位与中间位置的操作——在位序 1 插入 / 删除时改的也是头结点的指针域,语句与其他位置完全一样,不必特判修改头指针。这正是大题代码默认带头结点的原因。
查找:按序号取第 \(i\) 个元素只能从首元结点出发沿 next 走 \(i\) 步,按值查找同样逐个比较,两者都是 \(O(n)\)——单链表是顺序存取结构,不支持随机存取(呼应 2.2 的辨析)。但插入 / 删除在已拿到前驱指针 \(p\) 时只改两条指针,\(O(1)\)。
2.3.2 插入、删除与建表
后插(在 \(p\) 之后插入 \(s\)):s->next = p->next; p->next = s;——先接管后继、再改前驱,顺序不可颠倒。删除 \(p\) 的后继 \(q\):q = p->next; p->next = q->next; free(q);。指针变化见图 2-1。
设 p 指向单链表中某结点,q 是 p 的原后继(p->next == q),在 p 之后插入 s,能正确完成插入的是( )
A. p->next = s; s->next = p->next; B. s->next = p->next; p->next = s; C. s->next = p; p->next = s; D. p->next = s; s->next = q;
查看解答
B:s 先接管 q(此时 p->next 仍是 q),再让 p 指向 s,两步互不破坏。
A 是经典陷阱:先执行 p->next = s 后,s->next = p->next 等价于 s->next = s——s 自环、q 从此失联(内存泄漏 + 死循环);C 中 s->next 指向前驱方向,遍历陷入 p↔s 互指环。
D 依赖题目额外给出的 q 才成立,q 未保存时不可用——标准答案永远是 B。口诀「先接新、再改旧:凡用到 p->next 旧值的语句都放在改写它之前」。
建表有两种标准写法。头插法:新结点总插在表头,读入 \(a_1,\dots,a_n\) 得到的是逆序链 \(a_n\to\cdots\to a_1\);尾插法:附设尾指针 \(r\)(始终指向表尾),得到顺序链。两者都是 \(O(n)\)。
// 头插法与尾插法建表:数组 a[0..n-1] 依次读入,均带头结点
LinkList HeadCreate(int a[], int n) { // 头插法:结果 a[n-1] → … → a[0](逆序)
LinkList L = (LinkList)malloc(sizeof(LNode));
L->next = NULL; // 空表:这一句不能省
for (int i = 0; i < n; i++) {
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = a[i]; s->next = L->next; L->next = s; // 头插两步
}
return L;
}
LinkList TailCreate(int a[], int n) { // 尾插法:结果 a[0] → … → a[n-1](顺序)
LinkList L = (LinkList)malloc(sizeof(LNode));
LNode *r = L; // r 始终指向表尾结点
for (int i = 0; i < n; i++) {
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = a[i]; s->next = NULL; r->next = s; r = s;
}
return L;
}2.3.3 双指针技巧与单链表逆置
设计一个尽可能高效的算法,输出带头结点单链表中倒数第 \(k\) 个结点的值(\(k\) 为正整数;若 \(k\) 大于表长则报告失败)。要求只扫描一遍。
// 双指针「间隔 k」:q 先走 k 步,再与 p 同步前进
int FindKthFromEnd(LinkList L, int k) {
LNode *p = L->next, *q = L->next;
for (int i = 0; i < k; i++) { // q 先走 k 步
if (q == NULL) return -1; // k > 表长:失败
q = q->next;
}
while (q != NULL) { // q 到 NULL 时,p 恰在倒数第 k
p = p->next;
q = q->next;
}
return p->data;
} 查看解答
正确性:当 q 走到 NULL(越过表尾)时它共走了 \(n\) 步;p 晚出发 \(k\) 步,走了 \(n-k\) 步,停在第 \(n-k+1\) 个结点——从尾部数正好第 \(k\) 个。时间 \(O(n)\)(一遍扫描),空间 \(O(1)\)。
数值验证(\(n=5\),链 1→2→3→4→5,\(k=2\)):q 先走 2 步到结点 3;同步推进 \((p{=}1,q{=}3)\to(2,4)\to(3,5)\to(4,\mathrm{NULL})\) 停,\(p\) 指向结点 4 = 倒数第 2 ✓。再验 \(k=5\):q 先走到 NULL 恰需 5 步,同步循环 0 次,p 停在首元 1 = 倒数第 5 ✓;\(k=6\) 时 q 在第 6 步前已为 NULL,返回 −1 ✓。
套路总结:单链表「只能向前走」,凡涉及「倒数 / 距表尾」的量,就用两个指针把「距离差」先造出来——这一招在例 8(公共后缀)中再次登场。
将带头结点单链表就地逆置(辅助空间 \(O(1)\))。
// 思路:把原表结点逐个“摘下来头插”,头插天生逆序(见图 2-2)
void ReverseList(LinkList L) {
LNode *p = L->next, *q;
L->next = NULL; // 先把原表摘空,L 成为空表
while (p != NULL) {
q = p->next; // 暂存 p 的后继,防断链
p->next = L->next; // 头插两步(先接新、再改旧,同例 3)
L->next = p;
p = q; // 处理下一个
}
} 查看解答
数值验证(\(n=3\),链 1→2→3):第 1 轮摘 1 插空表 → L→1;第 2 轮摘 2 头插 → L→2→1;第 3 轮摘 3 头插 → L→3→2→1 ✓。时间 \(O(n)\)(每个结点摘、插各一次),空间 \(O(1)\)(只用 p、q 两个指针)。
易错:q = p->next 必须在头插之前保存——一旦执行 p->next = L->next,p 原来的后继就丢了。
2.3.4 双链表 / 循环链表 / 静态链表
prior 指针域,双向都可 \(O(1)\):
// 双链表结点类型
typedef struct DNode {
int data;
struct DNode *prior, *next; // 分别指向前驱与后继
} DNode, *DLinkList;在 \(p\) 之后插入 \(s\)(四步,顺序敏感):s->next = p->next; p->next->prior = s; s->prior = p; p->next = s;——前两句都用到旧的 p->next,必须放在改写它的第四句之前。删除 \(p\) 的后继 \(q\):p->next = q->next; q->next->prior = p; free(q);。
L->next == L;从任一结点出发都能遍历全表。设尾指针 r 时找头(r->next)与找尾(r)都是 \(O(1)\),表尾操作频繁时首选;两个循环单链表首尾衔接只需 \(O(1)\)。② 循环双链表:头结点的 prior 也指表尾。判空:
L->next == L(且 L->prior == L,两者等价)。
while (p != NULL),而写 while (p != L)(或 p->next != L,按需要停在哪一步选)。检查循环链表代码就盯一件事:还有没有把 NULL 当表尾。
// 静态链表:cursor = -1 表示表尾,0 号下标常作备用链表头
#define MaxSize 50
typedef struct {
int data; // 数据域
int cursor; // 游标:下一元素的下标(-1 表尾)
} SLinkList[MaxSize];写出下列链表的判空条件:(1) 带头结点的单链表;(2) 带头结点的循环单链表;(3) 带头结点的循环双链表;(4) 不带头结点的单链表。
查看答案
(1) L->next == NULL;(2) L->next == L(头结点指向自己);(3) L->next == L(此时必有 L->prior == L);(4) L == NULL(头指针本身为空——不带头结点时头指针就是首元指针)。
辨析关键:先问「有没有头结点」,再问「循环到谁」。把 (1) 的答案用到 (2) 上是最高频的错误。
判断正误:(1) 静态链表的插入删除不需要移动元素;(2) 静态链表能随机存取第 \(i\) 个元素;(3) 静态链表必须在不支持指针的语言中才能使用。
查看答案
(1) 对:只改前后两个元素的游标(逻辑下一跳),元素物理位置不动。(2) 错:仍要顺游标逐个走,顺序存取。(3) 错:是「常用于 / 适合」不支持指针的语言,不是「必须」——支持指针的语言同样能实现。
2.4 顺序表与链表:对比与算法设计实战 真题风格
2.4.1 顺序表 vs 链表选型
| 对比维度 | 顺序表 | 链表(单链表) |
|---|---|---|
| 逻辑结构 | 都是线性表——完全相同(区别只在存储) | |
| 存取方式 | 随机存取,按位 \(O(1)\) | 顺序存取,按位 \(O(n)\) |
| 空间 | 存储密度 1(无额外开销);静态分配容量固定,可能溢出或闲置;需一整块连续空间 | 存储密度 < 1(指针域开销);动态按需分配结点,不需要连续大块空间 |
| 插入 / 删除 | 需移动元素,平均 \(\frac{n}{2}\) / \(\frac{n-1}{2}\) 个,\(O(n)\) | 找到位置后改指针 \(O(1)\),但找位置本身 \(O(n)\) |
| 适用场景 | 表长可预估、按位访问频繁、插删少(如查表、随机取样) | 表长变化剧烈、频繁插删、只顺序扫描(如不确定长度的流式处理) |
2.4.2 顺序表算法实战
设 A、B 是元素递增有序的顺序表,写算法合并为递增有序表 C(值相同的元素都保留)。
// 归并:谁小谁先进 C——这是第 8 章归并排序的“原子操作”,现在就要掌握
bool Merge(SqList A, SqList B, SqList *C) {
if (A.length + B.length > MaxSize) return false; // 放不下
int i = 0, j = 0, k = 0;
while (i < A.length && j < B.length) // 两路都未取完
C->data[k++] = (A.data[i] <= B.data[j])
? A.data[i++] : B.data[j++];
while (i < A.length) C->data[k++] = A.data[i++]; // 扫尾:A 有剩余
while (j < B.length) C->data[k++] = B.data[j++]; // 扫尾:B 有剩余
C->length = k;
return true;
} 查看解答
每个元素恰好进 C 一次,时间 \(O(m+n)\)、空间 \(O(1)\)(C 是输出,不算辅助)。<= 保证相等时先取 A 的元素,合并稳定(A 中元素的相对次序不被 B 打乱)。
数值验证:A=\(\{2,5\}\),B=\(\{1,3,5\}\):比 \(2|1\) 取 1;比 \(2|3\) 取 2;比 \(5|3\) 取 3;比 \(5|5\) 取 A 的 5(相等取前路);A 尽,扫尾 B 的 5 → C=\(\{1,2,3,5,5\}\) ✓,共比较 4 次恰好每对元素最多比一次。
易错:忘记扫尾循环是最常见丢分点——一路取完后另一路的剩余段必须整体搬过去。
非递减有序顺序表中值相同的元素只保留一个,其余删除,要求 \(O(n)\) 时间、\(O(1)\) 空间。
// 快慢指针:i 守“已保留段”末尾,j 向前探查
int Dedup(SqList *L) {
if (L->length == 0) return 0;
int i = 0; // 慢指针:已保留段最后一个下标
for (int j = 1; j < L->length; j++) // 快指针:逐个检查
if (L->data[j] != L->data[i]) // 遇到新值
L->data[++i] = L->data[j]; // 就地覆盖到保留段之后
L->length = i + 1;
return L->length;
} 查看解答
数值验证(\(n=6\),\(\{1,2,2,3,3,3\}\)):
| \(j\) | 比较 data[j] 与 data[i] | 动作 | 保留段 |
|---|---|---|---|
| 1 | 2 ≠ 1 | data[1]=2,i=1 | \(\{1,2\}\) |
| 2 | 2 = 2 | 跳过 | \(\{1,2\}\) |
| 3 | 3 ≠ 2 | data[2]=3,i=2 | \(\{1,2,3\}\) |
| 4、5 | 3 = 3 | 跳过 | \(\{1,2,3\}\) |
最终 \(\mathrm{length}=i+1=3\),表为 \(\{1,2,3\}\) ✓。赋值只发生 2 次(每个「新值」一次)——有序表去重从不整体搬移,这是它优于无序表的原因。时间 \(O(n)\)、空间 \(O(1)\)。
将顺序表 \(L\) 中元素原地逆置(只允许 \(O(1)\) 辅助空间),写出核心代码并说明复杂度。
查看答案
对折交换(第 1 章例 7 已分析过同类问题):
void Reverse(SqList *L) {
for (int i = 0; i < L->length / 2; i++) {
int t = L->data[i];
L->data[i] = L->data[L->length - 1 - i];
L->data[L->length - 1 - i] = t;
}
} \(n=5\) 时交换 \((0,4)\)、\((1,3)\) 两轮,中间 data[2] 不动 \(\lfloor 5/2\rfloor=2\) ✓。时间 \(O(n)\),空间 \(O(1)\)——它也是自测 10(循环左移)的一个零件。
2.4.3 链表算法实战
两个带头结点单链表共享同一段后缀(形如 Y)。设计算法找出公共后缀的起始结点,要求不破坏两链、尽量高效。
// 思路:先各测表长,长链先走 |len1-len2| 步(例 4 的“造距离差”),再同步比较指针
int ListLen(LinkList L) {
int n = 0;
LNode *p = L->next;
while (p != NULL) { n++; p = p->next; }
return n;
}
LNode *FindFirstCommon(LinkList s1, LinkList s2) {
int m = ListLen(s1), n = ListLen(s2);
LNode *p = s1->next, *q = s2->next;
for (; m > n; m--) p = p->next; // s1 长则先走
for (; n > m; n--) q = q->next; // s2 长则先走
while (p != NULL && p != q) { // 同步推进,指针相等即命中
p = p->next;
q = q->next;
}
return p; // 无公共结点时返回 NULL
} 查看解答
正确性:两链一旦汇合就不再分开(每个结点只有一个 next),所以从公共起点到表尾的长度相同;把长的链先走 \(|\,\mathrm{len}_1-\mathrm{len}_2\,|\) 步后,两指针距表尾等远,同步前进时第一次「指针相等」的结点就是答案。时间 \(O(\mathrm{len}_1+\mathrm{len}_2)\)、空间 \(O(1)\)。
易错:公共后缀比较的是结点地址(p == q),逐个比 data 会误判——两链前段完全可能出现相同值而不相交。
套路总结:本题 = 例 4「先造距离差」的直接推广;结合 tip-box 的快慢指针,链表设计题的两大双指针模型(间隔 k、速度 2:1)就齐了。
用快慢指针写算法:返回带头结点单链表的中间结点(表长偶数时返回后半段起点),并分别对 \(n=5\)、\(n=6\) 各走一遍验证。
查看答案
LNode *FindMid(LinkList L) {
LNode *slow = L->next, *fast = L->next;
while (fast != NULL && fast->next != NULL) {
slow = slow->next; // 慢指针 1 步
fast = fast->next->next; // 快指针 2 步
}
return slow;
} 验证 \(n=5\)(1…5):轮次后 (s2,f3)→(s3,f5),f5 的 next 为 NULL 停 → 第 3 个 = 中点 ✓。\(n=6\)(1…6):(s2,f3)→(s3,f5)→(s4,f=NULL) 停 → 第 4 个 = \(\lfloor 6/2\rfloor+1\),即后半段起点 ✓。注意 fast != NULL 与 fast->next != NULL 的先后顺序——写反会在偶数表长时解引用 NULL。
2.5 章末自测 真题风格
限时 50 分钟,先做后看答案。难度:★★ 基础 / ★★★ 强化 / ★★★★ 冲刺。所有移动次数、指针推进类结论自检:代入小 \(n\)(3~6)逐步数一遍。
表长 \(n=8\) 的顺序表,在位序 3 处插入一个元素,需移动的元素个数是( )
A. 5 B. 6 C. 7 D. 8
查看答案
B。\(n-i+1=8-3+1=6\)(\(a_3\cdots a_8\) 依次后移)✓。顺手验极端:位序 1 插入动 8 个、位序 9(表尾后)动 0 个,均与公式吻合。
关于存取方式,下列说法正确的是( )
A. 链表支持随机存取 B. 顺序表是一种顺序存取的存储结构 C. 顺序表可以随机存取任意一个元素 D. 存取方式由线性表的逻辑结构决定
查看答案
C。A:链表只能从头顺序存取;B:「顺序表」的「顺序」指存储结构(物理相邻),它恰恰是随机存取的——名字与存取方式正好相反,是本考点的经典陷阱;D:存取方式由存储结构决定(对照 2.4 表:同为线性表,两实现存取方式不同)。
带表头结点 t 的单链表为空的判定条件是( )
A. t == NULL B. t->next == NULL C. t->next == t D. t->next->next == NULL
查看答案
B。带头结点时头指针 t 永远指向头结点,判空看首元:t->next 为 NULL。A 是不带头结点时的判据;C 是带头结点循环单链表的判据;D 表示表中恰剩一个首元结点(非空)。
双链表中 p 指向某结点,s 是新结点,在 p 之后插入 s。下列操作序列正确的是( )
① s->next = p->next ② p->next->prior = s ③ s->prior = p ④ p->next = s
A. ①②③④ B. ①③④② C. ④①②③ D. ②①③④
查看答案
A(另有多个合法变体,如 ③①②④)。硬约束:①②都读取旧的 p->next(取后继、访问其 prior 域),必须发生在改写它的④之前;B 的错误在于④执行后②访问的是 s->prior 而非「原后继的 prior」,原后继结点的 prior 域没被改,反向遍历在 p→s 处断链;C 首句就毁掉后继地址,非法。口诀:凡「用旧值」的语句一律放在「改旧值」之前。
与顺序表相比,静态链表的优点是( )
A. 可以随机存取任一元素 B. 插入删除时无需移动元素 C. 存储密度更高 D. 所需空间随表长动态增长
查看答案
B。插入删除只改游标(下标),元素物理位置不动。A:仍要顺游标逐个找,不能随机存取;C:游标域占额外空间,存储密度低于顺序表;D:容量由数组大小固定,不能动态增长。
顺序表首地址 \(\mathrm{LOC}(A)=1000\),每个元素占 6 个单元,则位序 10 的元素地址是 \(\underline{\hspace{1.5cm}}\)。
查看答案
位序 10 ↔ 下标 9:\(1000+9\times6=1054\) ✓(自检:下标 0 存 1000,下标 9 是第 10 个 6 单位格,跨过 \(9\times6=54\) 个单元)。
表长 \(n=101\) 的顺序表,等概率下:插入一个元素平均移动 \(\underline{\hspace{1.5cm}}\) 个;删除一个元素平均移动 \(\underline{\hspace{1.5cm}}\) 个。
查看答案
插入 \(\frac{n}{2}=50.5\) 个;删除 \(\frac{n-1}{2}=50\) 个。代入小 \(n\) 复核:\(n=3\) 时插入按位序 \(1\sim4\) 的移动数为 \(3,2,1,0\),均值 \(\frac{6}{4}=1.5=\frac{3}{2}\) ✓;删除按位序 \(1\sim3\) 的移动数为 \(2,1,0\),均值 \(1=\frac{3-1}{2}\) ✓。
带头结点的循环单链表判空条件是 \(\underline{\hspace{1.5cm}}\);带头结点的循环双链表判空条件是 \(\underline{\hspace{1.5cm}}\)。
查看答案
循环单链表:L->next == L;循环双链表:L->next == L(等价地 L->prior == L,空表时头结点前后指针都指回自己)。辨析口诀:单链表问「下一个是不是空」,循环链表问「下一个是不是自己」。
两个元素递增的单链表 A、B(均带头结点),写算法归并为一个递增单链表 C,要求利用原有结点空间(不再 malloc)。
查看解答
// 尾插归并:r 始终指向 C 的表尾,直接摘原结点接上
LinkList MergeList(LinkList A, LinkList B) {
LinkList C = A; // 借用 A 的头结点
LNode *p = A->next, *q = B->next, *r = C;
while (p != NULL && q != NULL) {
if (p->data <= q->data) { r->next = p; r = p; p = p->next; }
else { r->next = q; r = q; q = q->next; }
}
r->next = (p != NULL) ? p : q; // 扫尾:接上剩余段
free(B); // B 的头结点不再需要
return C;
} 每结点只接一次,\(O(m+n)\)、\(O(1)\)。与例 6 顺序表版对照:数组用下标 i/j/k,链表用指针 p/q/r,「比较—摘小—挂尾」三步完全同构;扫尾只需改一个指针(剩余段本来就连着)。易错点同样是忘扫尾。
(2010 真题原型)将 \(n\) 个整数存于数组 R,设计算法把 R 循环左移 \(p\ (0\lt p\lt n)\) 个位置(如 1 2 3 4 5 左移 2 位得 3 4 5 1 2),要求时间 \(O(n)\)、空间 \(O(1)\)。
查看解答
思路(三次逆置,练习 5 是零件):先把前 \(p\) 个逆置、再把后 \(n-p\) 个逆置、最后整体逆置。
// ab → (a^r b^r),再整体逆置得 ba,即左移 p 位
void Reverse(int R[], int left, int right) {
while (left < right) {
int t = R[left]; R[left] = R[right]; R[right] = t;
left++; right--;
}
}
void LeftShift(int R[], int n, int p) {
Reverse(R, 0, p - 1); // 前段逆置
Reverse(R, p, n - 1); // 后段逆置
Reverse(R, 0, n - 1); // 整体逆置
} 数值验证(\(n=5\),\(p=2\),R=1 2 3 4 5):① 逆置前 2 个 → 2 1 3 4 5;② 逆置后 3 个 → 2 1 5 4 3;③ 整体逆置 → 3 4 5 1 2,恰为左移 2 位 ✓(每个元素被交换 \(\le 2\) 次,总交换次数 \(\lfloor p/2\rfloor+\lfloor(n-p)/2\rfloor+\lfloor n/2\rfloor\lt\frac{3n}{2}\),时间 \(O(n)\)、空间 \(O(1)\))。
易错:三段逆置的区间端点(0..p−1 / p..n−1 / 0..n−1);若要右移 \(p\) 位等价于左移 \(n-p\) 位,直接换参数即可。
2.6 本章考点总结
| 考点 | 常考题型 | 热度 | 核心方法 |
|---|---|---|---|
| 头结点辨析(作用 / 判空) | 选择题 | ★★★★★ 高频 | 带头结点统一空表与首位操作;判空 L->next == NULL(单)、== L(循环);不带头结点看 L == NULL |
| 随机存取 vs 顺序存取 | 选择题 | ★★★★ 高频 | 顺序表 = 顺序存储但随机存取;链表 = 链式存储且顺序存取;名字与结论正好相反 |
| 插入 / 删除移动次数 | 选择 / 填空 | ★★★★★ 高频 | 插 \(n-i+1\)、删 \(n-i\) 个;平均 \(\frac{n}{2}\)、\(\frac{n-1}{2}\);插合法 \(1\le i\le n+1\)、删 \(1\le i\le n\);代小 \(n\) 验证 |
| 顺序表地址计算 | 填空 | ★★★ | \(\mathrm{LOC}(a_i)=\mathrm{LOC}(A)+(i-1)\cdot c\)(位序);下标 \(j\) 则无 \(-1\);先换算位序↔下标再算 |
| 单链表插入删除指针顺序 | 选择 / 大题 | ★★★★ | 「先接新、再改旧」:用旧 p->next 的语句放在改写它之前;删除先摘链后 free |
| 建表与逆置 | 大题 | ★★★★ | 头插得逆序(建表/逆置两用)、尾插得顺序(r 指尾);逆置 = 摘空 + 逐个头插 |
| 双指针模型 | 大题 | ★★★★★ 真题高发 | 间隔 k(倒数第 k、公共后缀:长链先走差值步);快慢 2:1(找中点、判环);快慢同步覆盖(有序去重) |
| 归并与循环左移 | 大题 | ★★★★ | 「比较—取小—挂尾」+ 别忘扫尾;左移 p 位 = 三段逆置(\(O(n)\)/\(O(1)\)) |
| 双链表 / 循环链表 / 静态链表 | 选择 / 判断 | ★★★ | 双链表四步插入(旧 p->next 先用后改);循环表遍历看 p != L;静态链表游标免移动、不能随机存取 |