第 2 章 线性表
408 数据结构第 2 章 线性表复习讲义,涵盖核心概念、重点考点与典型题型。
Last updated on
一、先建立整章框架
这一章其实围绕一个问题:
逻辑上是一串前后相邻的数据,到底怎样存进计算机?
逻辑结构都是同一个:
a1 → a2 → a3 → ... → an但可以有两种完全不同的物理实现。
1. 顺序存储
内存:
┌────┬────┬────┬────┐
│ a1 │ a2 │ a3 │ a4 │
└────┴────┴────┴────┘
地址连续这就是顺序表。
最大特点:
可以直接算出第几个元素在哪里。
所以随机访问快。
2. 链式存储
[a1 | next] → [a2 | next] → [a3 | next] → ...实际内存地址可能完全不连续:
1000: a1 → 8000
8000: a2 → 3000
3000: a3 → NULL这就是链表。
最大特点:
不靠物理位置表示先后关系,而靠指针。
所以插入删除灵活。
资料把链式结构进一步分成:
- 单链表
- 双链表
- 循环链表
- 静态链表
其中前三种通常通过指针实现,静态链表用数组下标模拟指针。
二、2.1 线性表的定义和基本操作
1. 什么叫线性表?
定义:
其中:
- 元素数量有限;
- 元素类型相同;
- 有明确先后顺序;
- 除第一个元素外,每个元素只有一个直接前驱;
- 除最后一个元素外,每个元素只有一个直接后继。
因此它是一种典型的一对一逻辑结构。
一个特别容易混淆的问题
线性表 ≠ 顺序表。
准确来说:
线性表
↓
逻辑结构而:
顺序表
链表
↓
存储结构例如:
逻辑:
A → B → C → D可以存成数组:
[A][B][C][D]也可以存成链表:
[A|·] → [B|·] → [C|·] → [D|NULL]所以:
算法研究逻辑关系,具体代码实现依赖存储结构。
2. 线性表有哪些基本操作?
资料列出的标准操作要熟悉:
InitList(&L) // 初始化
Length(L) // 求表长
LocateElem(L,e) // 按值查找
GetElem(L,i) // 按位查找
ListInsert(&L,i,e) // 插入
ListDelete(&L,i,&e)// 删除
PrintList(L) // 输出
Empty(L) // 判空
DestroyList(&L) // 销毁真正考试高频的其实集中在四个:
查找、插入、删除、遍历。
其他操作大多是它们的组合。
三、2.2 顺序表——★★★★★ 高频
资料明确标出“顺序表的应用”为多次算法题考点,因此这一部分必须重点掌握。
1. 顺序表的本质
顺序表就是:
用一段连续的内存空间按逻辑顺序依次存放元素。
假设:
a1 a2 a3 a4每个元素占 个字节。
若第一个元素地址为 ,则第 个元素:
这条公式解释了为什么顺序表能随机访问。
因为不用:
a1 → a2 → a3 → ...一个一个找。
只要知道 ,直接计算地址即可。
所以:
2. 位序和数组下标——408 高频坑
线性表:
第1个 第2个 第3个数组:
data[0] data[1] data[2]所以始终记住:
也就是说:
第 i 个元素 = data[i - 1]这是本章代码最常见的下标错误之一。
3. 静态顺序表与动态顺序表
静态分配
#define MaxSize 50
typedef struct {
ElemType data[MaxSize];
int length;
} SqList;特点:
最大容量一开始就确定。
例如:
MaxSize = 50
length = 18意味着:
已经用了18个
还能放32个如果:
length == MaxSize继续插入就失败。
动态分配
typedef struct {
ElemType *data;
int MaxSize;
int length;
} SeqList;通过:
L.data =
(ElemType *)malloc(sizeof(ElemType) * InitSize);动态申请空间。
很重要的理解
动态顺序表仍然是顺序表。
不是链表。
因为它仍然要求:
[a1][a2][a3][a4]物理连续。
只是当空间不够时,可以:
申请更大的连续空间
↓
复制原数组
↓
释放旧空间资料也特别提醒:动态分配改变的是容量管理方式,并不改变顺序存储的物理结构。
四、顺序表插入——★★★★★
这是必须会手写的基本操作。
假设:
A B C D E现在在第 3 个位置插入 X:
A B X C D E问题是:
C D E必须向后移动。
1. 为什么必须从后向前移动?
原数组:
下标
0 1 2 3 4
A B C D E插入 X 到第 3 位,即:
data[2]必须:
E → 5
D → 4
C → 3最后:
X → 2所以移动方向必须:
从右往左。
否则,如果从左向右:
C → D位置原 D 被覆盖。
然后:
D → E位置但 D 已经没了。
因此资料代码写成:
for (int j = L.length; j >= i; j--)
L.data[j] = L.data[j - 1];
L.data[i - 1] = e;
L.length++;2. 为什么循环条件是 j >= i?
假设:
length = 5
i = 3移动:
data[4] → data[5]
data[3] → data[4]
data[2] → data[3]也就是:
j = 5,4,3所以:
j >= i五、顺序表插入复杂度——★★★★★
设原来有 个元素。
最好情况
插到最后:
A B C D | X不移动元素。
所以:
最坏情况
插到最前:
X A B C D所有 个元素都移动。
所以:
平均情况
插入位置共:
种。
在第 个位置,需要移动:
个元素。
假定各位置等概率,则平均移动次数:
即:
因此:
这一类“平均移动多少元素”的题非常适合选择题。资料也明确把顺序表基本操作复杂度列为 2023 年考点。
六、顺序表删除——★★★★★
例如:
A B C D E删除 C:
A B D E这次需要:
D E向前移动。
所以方向反过来:
从左往右。
核心代码:
e = L.data[i - 1];
for (int j = i; j < L.length; j++)
L.data[j - 1] = L.data[j];
L.length--;删除复杂度
删除第 个元素:
需要移动:
个元素。
最好
删最后一个:
最坏
删第一个:
平均移动次数
所以:
因此:
七、顺序表查找
这里一定要区分两个概念。
1. 按位查找
问:
第 个元素是什么?
由于:
可以直接访问:
2. 按值查找
问:
值为 的元素在哪?
如果表没有其他条件:
从头比较:
A[0]
A[1]
A[2]
...所以平均:
最坏:
如果顺序表有序呢?
就可以折半查找。
因此:
这一点是顺序表相比普通链表的重要优势。
因为折半查找需要直接访问:
中间元素顺序表:
A[mid]即可。
链表则不能直接得到第 mid 个结点。
八、顺序表最核心的考试理解
可以记成四个字:
查快,改慢。
按位置访问:
插入删除:
原因:
地址连续
↓
可以直接算位置
↓
但改变位置需要整体搬家九、2.3 单链表——★★★★★ 整章核心
资料的考点追踪显示,单链表应用在多届统考中反复出现,因此这是第 2 章最重要的部分之一。
1. 单链表结点
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;每个结点:
┌──────┬──────┐
│ data │ next │
└──────┴──────┘其中:
data:数据next:下一个结点地址
2. 为什么链表不需要连续地址?
例如:
地址1000:
[A | 5000]
地址5000:
[B | 2600]
地址2600:
[C | NULL]逻辑结构仍然是:
A → B → C因为:
A.next = 5000
B.next = 2600所以链表的本质就是:
用指针代替物理相邻关系。
十、头指针、头结点——★★★★★ 高频易错
这一部分经常混。
1. 头指针
头指针:
指向链表第一个结点的指针。
例如:
L
↓
[A] → [B] → [C]这里 L 是头指针。
2. 头结点
人为增加一个结点:
L
↓
[head] → [A] → [B] → [C]head 本身通常不存有效数据。
此时:
L 指向头结点
L->next 指向第一个数据结点3. 带头结点空表
L
↓
[head | NULL]判空:
L->next == NULL4. 不带头结点空表
L == NULL十一、为什么要加头结点?
这是非常重要的“为什么”。
假设没有头结点:
L → A → B → C现在在 A 前面插 X:
L → X → A → B → C必须修改:
L = X;但如果是在 B 前插 X:
A → X → B只修改普通结点指针即可。
也就是说:
首结点操作和普通结点操作不统一。
加入头结点以后:
L → head → A → B → C在 A 前插 X:
head → X → A本质与:
A → X → B完全一样。
因此头结点最大的价值是:
统一边界操作。
资料明确强调了这一点。
十二、单链表查找
1. 按位查找
想找第 个数据结点。
由于链表不能随机访问:
head → a1 → a2 → ... → ai必须一个一个走。
因此:
核心形式:
LNode *p = L->next;
int j = 1;
while (p != NULL && j < i) {
p = p->next;
j++;
}2. 按值查找
LNode *p = L->next;
while (p != NULL && p->data != e)
p = p->next;也是:
十三、单链表插入——★★★★★
假设:
p → A → B在 A 后插入 X:
p → A → X → B设:
p 指向 A
s 指向 X核心两句:
s->next = p->next;
p->next = s;为什么顺序不能反?
原来:
p
↓
A ─────→ B第一步:
s->next = p->next;变成:
A → B
X → B第二步:
p->next = s;得到:
A → X → B如果反过来:
p->next = s;
s->next = p->next;执行第一句后:
A → X原来的 B 地址已经丢了。
第二句此时:
s->next = s;于是:
X → X形成自环。
模拟题也专门考过这一点:带头结点链表用头插法时,正确顺序是先令新结点指向原首结点,再令头结点指向新结点。
十四、为什么“已知 p 后插”是 ,而“第 i 位插入”是 ?
这是一个非常容易答错的问题。
如果题目说:
已知结点
p,在p后插入。
只需要:
s->next = p->next;
p->next = s;所以:
但是如果说:
在第 个位置插入。
你首先要找到第 个结点:
head → 1 → 2 → ... → i-1查找本身:
虽然修改指针只有:
总复杂度仍是:
所以考试中必须看清楚:
操作位置是否已经给出。
十五、单链表前插的 技巧
假设题目给你结点:
p
↓
A → B要求:
在
p前插入 X。
单链表没有 prior 指针。
普通做法必须从头找前驱:
但资料给出了一个非常巧妙的技巧。
先在 p 后插新结点:
A → X → B然后交换 A、X 的数据:
X → A → B从逻辑数据序列看:
X
A
B就相当于在原 A 前插入 X。
代码:
s->next = p->next;
p->next = s;
ElemType temp = p->data;
p->data = s->data;
s->data = temp;因此若数据可复制,可实现:
这是很典型的“改数据代替改结构”思想。
十六、单链表删除——★★★★★
假设:
A → B → C删除 B。
设:
p 指向 A
q 指向 B只需:
q = p->next;
p->next = q->next;
free(q);图示:
删除前:
p
↓
A → B → C
↑
q执行:
p->next = q->next;变为:
A ─────→ C
B然后:
free(q);十七、“删除给定结点 p”的 技巧——★★★★★
这是链表算法中非常经典的技巧。
假设:
A → B → C → D
↑
p如果直接删 B,本来需要知道 A。
但单链表中:
B不知道前驱 A。
正常必须从头找:
技巧
把 C 的数据复制到 B:
A → C → C → D
↑
p再删除后面的 C:
A → C → D
↑
p逻辑效果:
原来的 B 消失了。
代码:
LNode *q = p->next;
p->data = q->data;
p->next = q->next;
free(q);所以:
但有一个重要限制
如果:
p
↓
D → NULL即 p 是尾结点。
就不能这么做,因为:
p->next == NULL没有后继可以复制。
十八、头插法建立单链表——★★★★★
输入:
1 2 3 4头插法过程:
先插 1:
head → 1插 2:
head → 2 → 1插 3:
head → 3 → 2 → 1插 4:
head → 4 → 3 → 2 → 1最终:
4 3 2 1所以必须牢牢记住:
头插法会逆序。
核心:
s->next = L->next;
L->next = s;十九、尾插法建立单链表——★★★★★
输入:
1 2 3 4要求:
1 → 2 → 3 → 4就必须不断插到尾部。
但如果每次都从头找尾:
每个元素一次:
所以增加一个尾指针:
LNode *r = L;每次:
r->next = s;
r = s;这样每次插入:
建立整个表:
因此:
尾插法正序,必须维护尾指针。
资料总结也把“头插逆序、尾插正序”列为本章必须掌握的对比。
二十、链表逆置——★★★★★ 算法题核心思想
资料把“逆置法”列为链表算法题常用技巧。
假设:
1 → 2 → 3 → 4 → NULL逆置:
4 → 3 → 2 → 1 → NULL经典做法需要三个指针:
pre
p
r其中:
pre:已经逆置好的部分p:当前结点r:保存后继
为什么要保存 r?
假设:
pre p
↓ ↓
1 ← 2 → 3 → 4如果直接:
p->next = pre;原来的:
3 → 4就找不到了。
所以必须先:
r = p->next;再:
p->next = pre;最后推进:
pre = p;
p = r;完整核心:
LNode *pre = NULL;
LNode *p = L->next;
LNode *r;
while (p != NULL) {
r = p->next;
p->next = pre;
pre = p;
p = r;
}
L->next = pre;复杂度:
辅助空间:
链表题里最重要的原则之一就是:
改
next之前,先想清楚原来的next以后还需不需要。
资料的复习总结也特别强调了“指针修改顺序”这一点。
二十一、2.3.3 双链表——★★★★
单链表最大的缺陷:
只能知道后继,不知道前驱。
所以定义:
typedef struct DNode {
ElemType data;
struct DNode *prior;
struct DNode *next;
} DNode;结构:
← prior | data | next →例如:
A ⇄ B ⇄ C对 B:
B->prior = A
B->next = C二十二、双链表插入——★★★★★ 指针顺序题
资料标出 2023 年曾考双链表插入。
现在:
A ⇄ B
↑
p在 A 后插入 X:
A ⇄ X ⇄ B设:
p = A
s = X标准思路:
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;理解它,不要死背。
新结点 X 有两个邻居:
A ← X → B所以:
X.prior = A
X.next = B同时:
A.next = X
B.prior = X总共就是四根指针。
为什么 s->next = p->next 必须在 p->next = s 前?
因为:
p->next原本保存 B 的地址。
如果先:
p->next = s;那么:
B 的地址就丢了之后再:
s->next = p->next;得到的就是:
s->next = s;因此,本质还是同一句话:
先保存旧关系,再建立新关系。
模拟选择题也专门设置了双向循环链表中“在 p 前插 x”的语句顺序判断。
二十三、双链表删除——★★★★
假设:
A ⇄ B ⇄ C
↑ ↑
p q删除 B:
p->next = q->next;
q->next->prior = p;
free(q);得到:
A ⇄ C如果直接给出了:
q = B双链表可以通过:
q->prior直接得到 A。
因此删除给定结点一般可以做到:
而单链表通常还需要找前驱。
二十四、单链表和双链表的本质区别
| 项目 | 单链表 | 双链表 |
|---|---|---|
| 后继 | 容易 | 容易 |
| 前驱 | 需遍历 | 直接得到 |
| 指针空间 | 少 | 多 |
| 插删维护 | 简单 | 较复杂 |
| 双向遍历 | 不行 | 可以 |
所以:
双链表是用更多空间换更方便的前驱访问。
二十五、2.3.4 循环链表——★★★★
1. 普通单链表
尾结点:
an → NULL2. 循环单链表
尾结点:
an
↓
头结点即:
head → a1 → a2 → a3
↑ ↓
└────────────────┘因此:
r->next = L;二十六、循环链表判空——高频易错
带头结点普通单链表:
L->next == NULL循环单链表空表:
L
↺头结点自己指向自己。
所以:
L->next == L而不是:
L->next == NULL资料标出“循环单链表删除首元素”在 2021 年出现过考点。
二十七、为什么循环单链表常只设尾指针?
假设:
r
↓
head → A → B → C
↑ ↓
└────────────┘如果只知道尾指针 r:
尾结点:
r第一个结点或头部:
r->next因此:
- 找尾部:
- 找头部:
特别适合需要频繁操作首尾的场景。
模拟题中也会结合“只有尾指针的循环单链表”考出队、入队等复杂度判断。
二十八、循环单链表尾插
原来:
... → r
↓
head插入 s。
第一步:
s->next = r->next;即:
s → head第二步:
r->next = s;第三步:
r = s;最终:
旧尾 → 新尾 → head整个操作:
二十九、循环双链表
普通双链表:
NULL ← head ⇄ A ⇄ B ⇄ C → NULL循环双链表:
┌──────────────┐
↓ │
head ⇄ A ⇄ B ⇄ C
↑ ↓
└──────────────┘头结点:
L->prior指向尾结点。
尾结点:
tail->next指向头结点。
空循环双链表
┌──────┐
↓ │
head
↑ │
└──────┘因此:
L->next == L
L->prior == L三十、2.3.5 静态链表——★★
这一部分理解即可,但选择题可能直接考“数组下标模拟指针”。
普通链表:
struct Node {
data;
Node *next;
};静态链表:
struct Node {
data;
int next;
};注意:
next 不是地址而是:
下一个结点的数组下标。
例如:
| 下标 | data | next |
|---|---|---|
| 0 | 2 | |
| 1 | b | 6 |
| 2 | a | 1 |
| 3 | d | -1 |
| 6 | c | 3 |
从 0 开始:
0.next = 2所以首数据:
下标2 → a然后:
2.next = 1所以:
a → b再:
1.next = 6所以:
b → c最后:
6 → 3 → -1得到:
a → b → c → d典型选择题
资料题库中有一道:
若逻辑上第 个结点数组下标是 ,第 个结点数据是什么?
因为:
S[i].link是后继下标。
所以:
S[S[i].link].data这就是静态链表最核心的考法。
三十一、顺序表 vs 链表——★★★★★ 必背但要理解
这一表建议真正理解。
| 操作 | 顺序表 | 单链表 |
|---|---|---|
| 按位访问 | ||
| 无序按值查找 | ||
| 有序查找 | 可折半 | 通常仍 |
| 已知位置后插 | 常需移动 | |
| 删除已知前驱的后继 | 常需移动 | |
| 存储空间 | 连续 | 可不连续 |
| 每个元素额外空间 | 无 | 需要指针 |
| 扩容 | 不方便 | 灵活 |
资料将二者的选择原则总结为:规模稳定、随机访问多时更适合顺序表;规模变化大、插入删除多时更适合链式存储。
三十二、最容易错的一句话
很多人会说:
链表插入删除是 。
这句话不完整。
正确说法:
如果插入/删除位置已经确定,修改指针是 。
如果题目是:
删除第 个元素。
还需要先找:
第 i-1 个结点所以:
因此实际复杂度必须拆成:
定位成本 + 修改成本这是本章特别重要的复杂度分析思想。
三十三、本章算法题真正考什么?
资料总结得非常明确:算法设计题通常要求三部分:
- 写算法思想;
- C/C++ 描述;
- 分析时间、空间复杂度。
而常见技巧集中在:
顺序表:
双指针
归并
排序
二分查找
链表:
头插
尾插
逆置
归并
双指针三十四、顺序表高频套路 1:双指针删除
例如:
删除顺序表中所有值为 的元素。
最笨办法:
发现一个:
后面全部前移假设很多元素都等于 ,可能变成:
更优方法
两个指针:
i:扫描原数组
k:记录保留元素应放的位置例如:
1 2 3 2 4 2 5删 2。
扫描:
1 → 保留
3 → 保留
4 → 保留
5 → 保留逐个写回:
1 3 4 5代码思想:
int k = 0;
for (int i = 0; i < L.length; i++) {
if (L.data[i] != x)
L.data[k++] = L.data[i];
}
L.length = k;复杂度:
核心思想:
不要每删一个就整体移动,而是一次扫描完成压缩。
三十五、顺序表高频套路 2:有序表去重
例如:
1 1 2 2 2 4 5 5结果:
1 2 4 5因为有序,相同元素必然连续。
设:
i:最后一个保留元素
j:扫描指针若:
A[j] != A[i]说明发现新值:
A[++i] = A[j];最终长度:
i + 1时间:
空间:
三十六、高频套路 3:两个有序表归并
例如:
A: 1 3 6 9
B: 2 4 5 8设置:
i 指向 A
j 指向 B比较:
A[i] < B[j]谁小拿谁。
过程:
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5
...如果长度分别为:
则:
这就是后续归并排序等算法的基础。
三十七、链表高频套路:双指针
资料的复习总结也把双指针、快慢指针列为本章重要算法技巧。
1. 找倒数第 个结点
例如:
1 → 2 → 3 → 4 → 5找倒数第 2 个:
4设置:
fast
slow先让:
fast领先 个结点。
然后一起移动:
slow → ...
fast → ...当:
fast到尾时:
slow正好在倒数第 个。
复杂度:
空间:
2. 找链表中点
slow 每次走1步
fast 每次走2步例如:
1 → 2 → 3 → 4 → 5开始:
slow = 1
fast = 1第一次:
slow = 2
fast = 3第二次:
slow = 3
fast = 5fast 到末尾。
所以:
slow = 中点3. 判断是否有环
同样:
slow 每次1
fast 每次2如果有环:
快指针最终一定会追上慢指针。
如果:
fast == slow则有环。
这一思想后续图、链表综合算法也会不断出现。
三十八、链表高频套路:两个链表的公共结点
假设:
A: 1 → 2 → 7 → 8 → 9
↑
B: 4 → 5 ┘如果是真正共享结点,从第一次公共结点开始,后面必然全部相同。
原因很简单。
对结点 7:
7.next只有一个值。
不可能:
在 A 中指向8
在 B 中又指向另一个节点一种典型方法:
- 求 A 长度 ;
- 求 B 长度 ;
- 长链先走:
步; 4. 然后一起移动; 5. 第一次:
p == q的位置就是公共结点。
复杂度:
三十九、算法题如何写才能拿分?
资料明确提醒:408 算法题并不要求你写工业级、完全可运行的程序,核心是算法思想与关键代码。
推荐固定格式。
第一步:写算法思想
例如逆置:
设置三个指针
pre、p、r。pre指向已逆置部分,p指向当前待处理结点,r暂存p的后继。逐个修改当前结点的next指向前驱,直至链表结束。
第二步:代码
while (p != NULL) {
r = p->next;
p->next = pre;
pre = p;
p = r;
}第三步:复杂度
每个结点访问一次:
仅使用若干指针:
这样写非常清晰。
四十、本章几个经典选择题陷阱
题库里第 2 章的选择题重点也非常集中。
陷阱 1:头插语句顺序
正确:
p->next = L->next;
L->next = p;不能倒。
陷阱 2:静态链表后继
若:
i是当前结点数组下标。
后继下标:
S[i].link后继数据:
S[S[i].link].data陷阱 3:双链表修改旧指针过早
例如前插:
x->next = p;
x->prev = p->prev;
p->prev->next = x;
p->prev = x;如果过早:
p->prev = x;后面:
p->prev已经不再表示原前驱。
四十一、本章复杂度总表——考前必须能秒答
| 操作 | 顺序表 | 单链表 |
|---|---|---|
| 按位访问 | ||
| 无序按值查找 | ||
| 表头插入 | ||
| 表尾插入 | (容量足够) | 有尾指针 ,否则 |
| 已知前驱后插 | 不适用类似优势 | |
| 删除首元素 | ||
| 删除尾元素 | 普通单链表 | |
| 已知非尾结点删除 | 顺序表仍需移动 | 可用复制后继技巧 |
注意:
顺序表的 尾插只讨论容量已足够;若动态数组发生扩容,还需要重新申请空间并复制数据。
四十二、把这一章压缩成 8 个必须掌握的结论
资料本身也把复习重点压缩成类似几组对比。
1 线性表是逻辑结构
顺序表、链表才是存储结构。
2 顺序表
核心:
连续存储因此:
随机访问快
插入删除慢3 单链表
核心:
用 next 表示逻辑后继因此:
无需连续空间
不能随机访问4 头结点
不是第一个数据结点。
作用:
统一首结点和普通结点操作。
5 头插 / 尾插
头插 → 逆序
尾插 → 正序6 修改链表指针最重要的原则
先保存后继,再改指针。
例如:
r = p->next;
p->next = pre;
p = r;7 链表的 插删有前提
已经知道操作结点或其前驱。
否则定位:
8 顺序表和链表的根本权衡
顺序表:
用“连续空间”换随机访问能力。
链表:
用“额外指针空间”换动态插删能力。
四十三、408 复习优先级
按照资料中的考点追踪和本章性质,建议这样分配时间:
第一梯队,必须熟练手写:
- 顺序表插入、删除及复杂度;
- 单链表查找、插入、删除;
- 头插法、尾插法;
- 单链表逆置;
- 指针修改顺序;
- 顺序表与链表复杂度比较;
- 常见双指针思想。
第二梯队,选择题和算法题都可能出现:
- 双链表插入、删除;
- 循环单链表;
- 尾指针;
- 头结点与头指针;
- 删除给定非尾结点的技巧。
第三梯队,以理解和选择题为主:
- 静态链表;
- 动态顺序表的实现细节;
- 一些初始化、销毁接口定义。
如果把第 2 章真正吃透,最关键的能力不是“记住几十段代码”,而是看到链表图以后,能立即判断:当前有哪些指针关系,我要断哪条边、连哪条边、哪条旧边必须先保存。这才是后面链表算法题能够稳定做出来的核心。