C语言学到中段,很多人就开始撞上"数据结构"这堵墙。顺序表这名字听着很理论,说白了就是让数据在内存里连续排好队,像学校食堂排队打饭、书架上挨着放一排书,每个人都有自己的固定位置。C语言的数组天然就是顺序表的底子,所以它几乎成了学数据结构绕不开的第一道门槛。
这篇文章从头到尾只讲顺序表:它长什么样、为什么下标从0开始、插入删除时数据到底怎么挪、怎么初始化、怎么避免越界和内存泄漏。我会尽量用图的方式拆解内存布局和数据移动过程,不整那些云里雾里的理论。适合刚学数据结构的同学、期末复习的本科生,还有准备计算机二级但被指针和结构体弄得头疼的人,读完后你至少能手写出一个能跑的动态顺序表。
1. 先搞清顺序表在内存中的“长相”和数组下标原理
1.1 连续内存是顺序表的命根子
顺序表的核心只有两个字:连续。数据元素一个接一个地存放在一段地址连续的存储单元里,中间不允许断。这个特性决定了它在内存中的"长相",也决定了它的优点和短板。
text复制内存地址(低 -> 高)
+------+------+------+------+------+------+
| a[0] | a[1] | a[2] | a[3] | a[4] | ... |
+------+------+------+------+------+------+
2 5 9 7 8
^ ^
首元素地址 最后一个有效元素
length = 5 // 当前有5个有效元素
用生活里的例子来说,顺序表就像一栋楼里门牌号连续的5个房间,只要知道第一间的门牌,后面每一间的位置都能靠"第一间 + 偏移量"算出来。链表则像一群朋友散布在城市的各个角落,每个人手里拿着下一个人的地址纸条,想找第三个人,必须从第一个人开始挨个问过去——所以顺序表最大的招牌就是"随机访问快":给你一个下标,立刻就能找到对应元素,不用从头遍历。
理解了"连续"这个字,后面所有的插入、删除、扩容逻辑都围绕同一件事展开:如何保证数据依旧连续。如果中间空了一个位置,就不满足顺序表的定义了;如果尾部不够塞新数据,就得另外找一块更大的连续空间,把老数据整体搬过去。
1.2 下标从0开始不是任性,是公式算出来的
数组下标为什么从0开始,很多刚学C语言的人都会犯嘀咕。其实这是由地址计算公式决定的。假设顺序表首元素的内存地址是 base,每个元素占 size 个字节,那么第 i 个元素的地址就是:
text复制addr[i] = base + i * size
下标 i 正好是从首地址开始"走"了几步。你看,找第0个元素,偏移量就是0,base + 0 * size 就是自己,非常直观。如果硬要把第一个元素叫“第1个”,公式就会变成 base + (i - 1) * size,每访问一个元素都得先减1,白白多一次运算。当年设计C语言的工程师把下标定为0,本质上是贴近硬件寻址方式的一种“强迫症”,这个习惯也被Java、Python等语言继承了下来。
还有一点值得连在一起理解:C语言编译器处理数组下标时并不会检查你是否越界。a[5] 在语法上合法,运行时却可能踩到别的数据的内存。因为编译器只会忠实地按基地址+偏移量去取数据,至于这个地址是不是属于你的数组,它不关心。这既是C语言灵活高效的原因,也是大量内存bug的源头,后续写顺序表时,所有边界检查都得自己动手。
1.3 和链表第一次见面怎么选
在学习顺序表的时候,老师总会顺便提到链表,讲着讲着就把人绕晕。列一张最简单直接的对比表,后面写代码时心里能有个数:
| 对比维度 | 顺序表 | 链表 |
|---|---|---|
| 存储空间 | 一段连续内存 | 节点散落,靠指针连接 |
| 按下标访问 | O(1),直接算地址 | O(n),必须从头遍历 |
| 插入/删除 | 平均要移动约一半元素 | 只要修改指针,不需要搬数据 |
| 额外空间 | 基本无浪费(扩容时有少量空闲) | 每个节点多存一个指针 |
| CPU缓存友好性 | 高,连续数据预读效率好 | 低,节点跳跃访问 |
这张表解决了一个常见困惑:"那链表比顺序表好?"并非如此。如果业务场景主要是"查一下第几个元素",顺序表几乎是碾压级优势;如果场景是频繁在中间插入、删除大量数据,链表才更合适。现实工程里两者搭配得很多,下一篇写链表的时候我再展开,今天先把顺序表的每个操作抠明白。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手写前的准备:结构体设计、初始化和内存分布
2.1 用结构体把“三个关键零件”捆在一起
自己动手写顺序表,一定不能用“裸数组”硬扛,因为只靠一个数组记录不了有效元素个数,也记录不了当前最多能装多少。这里需要用结构体把三样东西绑在一起:
- 指向堆区数组的指针 data,用来存放元素;
- 整形变量 length,表示当前有多少个有效元素;
- 整形变量 capacity,表示当前这段内存最多能容纳多少个元素。
c复制#include <stdio.h>
#include <stdlib.h>
#define INIT_CAPACITY 10
typedef int ElemType; // 让类型有通用性,以后改成double、结构体都方便
typedef struct {
ElemType *data; // 指向一段连续内存的首地址
int length; // 当前有效元素个数
int capacity; // 当前容量
} SeqList;
为什么需要 capacity 和 length 两个量并存,而不是直接用数组的固定长度?因为顺序表在实际使用中不可能永远不扩容,如果创建时最多只能放10个元素,用户往里面塞第11个,程序直接崩掉,这样用起来太脆弱。动态分配内存后,capacity 代表"这块内存能装多少",length 代表"已经用了多少",只有当 length == capacity 时才知道需要扩容。少了任何一个量,代码都会变得很被动。
使用 typedef 把 int 换成 ElemType 也是写数据结构的一个通用习惯。顺序表可以装整数、浮点、结构体、字符串,只要改这一行,后续逻辑都不用动。刚学的时候可能觉得多此一举,一旦写到学生管理系统这类需要存储结构体数据的应用,你就能体会到这种“抽象一层”的好处。
2.2 初始化、销毁和malloc/free的成对使用
初始化函数做的不是简单赋0,而是要真正从堆区申请一块内存。在C语言的标准动态内存分配函数中,malloc 是使用频率最高的:
c复制void InitSeqList(SeqList *L) {
L->data = (ElemType *)malloc(INIT_CAPACITY * sizeof(ElemType));
if (L->data == NULL) {
printf("内存分配失败\n");
exit(1);
}
L->length = 0;
L->capacity = INIT_CAPACITY;
}
void DestroySeqList(SeqList *L) {
free(L->data);
L->data = NULL; // 防止产生悬空指针
L->length = 0;
L->capacity = 0;
}
malloc 成功之后会返回一段连续内存的首地址,失败则返回 NULL。很多教材为了篇幅会省略这段检查,但在真实工程里,“如果malloc失败直接往下用”几乎是必炸的写法。尤其嵌入式设备或长时间运行的服务端程序,内存分配失败不是小概率事件。第二行代码里对 L->data 判空就是为了不让程序带着空指针继续跑。
销毁时 free(L->data) 后紧接着把 data 手动置成 NULL 是防“悬空指针”的标准操作。如果不置空,指针里保存的还是那块已经还给操作系统的地址,一旦后续代码错误地再次访问或再次 free,轻则报段错误,重则造成内存管理混乱,极难排查。我一直习惯在销毁后把结构体里所有字段都重置一下,虽然代码多了两行,但避免了几个月之后回头调试时被自己不规范的写法坑。
2.3 画一张图,看清单个顺序表实例在内存里的全貌
很多初学者背得住结构体定义,却从没在脑子里建立起“结构体变量里面装的是什么”的画面。这里用一张简单的示意图把它们串起来:
text复制栈区 堆区
+------------------+ +------+------+------+------+------+------+
| SeqList L | | data[0] | data[1] | ... | data[9] |
| data --------+---------->| 0 | 0 | ... | 0 |
| length = 0 | +------+------+------+------+------+------+
| capacity = 10 |
+------------------+
main函数里声明了 SeqList L,接着调用 InitSeqList(&L)
后,data指向堆上能放10个int的连续区域。
数据真正存放的位置在堆区,而不是栈区。栈上那个 L 变量里只存了三样小东西:一个指针、两个整数。为什么数据不直接放在结构体里?因为如果结构体内嵌一个固定数组 int data[1000],这1000个int就会全塞在栈上,而默认的栈空间通常是几MB,根本经不起你创建好几个大顺序表。动态分配则把大块内存放到堆上,由你自己决定什么时候释放,灵活度高出一大截。
栈和堆的区别不展开讲太深,你只需要记住一句话:函数里局部变量默认在栈区,系统自动分配自动释放;malloc 出来的内存在堆区,必须由程序员手动 free,不然程序不退出就一直占着。顺序表真正的数据体积通常是动态的,所以让 data 指针指向堆区是必然选择。
3. “图解析”顺序表的三大核心操作:插入、删除、查找
3.1 插入操作:为什么必须从最后一个元素开始往后挪
在顺序表里放新元素,最核心的动作就是“搬数据”。假设现在有一个包含5个元素的顺序表:[2, 5, 9, 7, 8],我要在第3个位置(位序为3,即下标2)插入一个新元素6。必须先把下标2及其后面的元素逐个往后挪一个位置,腾出空位后再写入6。
text复制原始:
下标 0 1 2 3 4
数据 2 5 9 7 8
第一轮:把下标4的8移到下标5
2 5 9 7 [ ] 8
第二轮:把下标3的7移到下标4
2 5 9 [ ] 7 8
第三轮:把下标2的9移到下标3
2 5 [ ] 9 7 8
最后:把6放到下标2
2 5 6 9 7 8
这里有个特别容易写错的地方:为什么必须从最后一个有效元素开始往后移动,而不是先移动最前面的元素?你自己模拟一下就明白了。如果先把下标2的9移到下标3,那么原来下标3的7就被覆盖了,等你再想移动7时,它已经不见了。所以“从后往前挪”是插入操作不可违背的纪律。
代码如下,我加了完整的边界检查:
c复制int SeqListInsert(SeqList *L, int pos, ElemType e) {
// pos表示位序,从1开始
if (pos < 1 || pos > L->length + 1) {
printf("插入位置非法\n");
return 0;
}
// 如果满了,先扩容
if (L->length == L->capacity) {
// 扩容函数,下面会单独讲
if (!ExpandSeqList(L)) return 0;
}
// 从最后一个有效元素开始,逐个后移
for (int i = L->length; i >= pos; i--) {
L->data[i] = L->data[i - 1];
}
L->data[pos - 1] = e;
L->length++;
return 1;
}
为什么插入位置上限是 length + 1?因为可以在最后一个元素后面追加,这对应 length+1 这个位序;为什么不能是 length + 2?因为中间空太多位置,就不满足“连续”的定义了。循环里 data[i] = data[i-1] 就是把前面那个位置的元素搬过来,i 从 length 开始,一直搬到 pos,最后 pos-1 这个位置自然空出来。
插入算法的平均时间复杂度是 O(n)。最好情况是表尾插入,移动0次,时间复杂度O(1);最坏情况是表头插入,需要移动全部 n 个元素;平均下来大约要移动 n/2 个元素,所以是 O(n)。这个结论考试常考,但更重要的是你在写代码时能感受到“为什么顺序表不擅长在中间插入”,因为每一次插入背后都是一场数据搬运。
3.2 删除操作:为什么必须从前往后挪
删除操作的逻辑刚好是插入的镜像。比如删除第3个元素,也就是下标2的那个6:
text复制删除前:
下标 0 1 2 3 4
数据 2 5 6 9 7
第1轮:把下标3的9搬到下标2
2 5 9 9 7
第2轮:把下标4的7搬到下标3
2 5 9 7 7(尾部残留旧值7,但length变成4,视为无效)
删除后 length 减1,但数组最后一个位置还残留原来的值7。这个残留不影响逻辑,因为所有遍历都只关心前 length 个元素。如果你有强迫症,可以额外把最后一个位置置0,但通常没有必要,反而多花时间。
删除时为什么必须从前往后搬?和插入同理,如果从最后一个元素开始往前搬,会把前面还没搬走的元素覆盖掉。删除的方向必须是:被删位置后面的元素,一个接一个往前顶。
c复制int SeqListDelete(SeqList *L, int pos, ElemType *e) {
if (pos < 1 || pos > L->length) {
printf("删除位置非法\n");
return 0;
}
*e = L->data[pos - 1]; // 先把被删元素保存出去
// 从被删位置的下一个元素开始,依次前移
for (int i = pos; i < L->length; i++) {
L->data[i - 1] = L->data[i];
}
L->length--;
return 1;
}
删除的时间复杂度也是 O(n)。最好情况删表尾,移动0次;最坏情况删表头,移动n-1次;平均移动 (n-1)/2 次。如果面试中被问到“为什么顺序表删除效率低”,你把移动次数报出来,再补一句“根本原因是必须维持物理连续”,基本就能得满分了。
3.3 按下标访问和按值查找,复杂度天差地别
顺序表有一个天然优势:按下标访问元素是真正的 O(1)。因为地址可以靠公式直接算出来,不需要任何遍历。这个特性在写二分查找、堆排序、哈希表时特别重要。代码上只需要一个 GetElem:
c复制ElemType GetElem(SeqList *L, int pos) {
if (pos < 1 || pos > L->length) {
printf("访问位置非法\n");
return -1;
}
return L->data[pos - 1];
}
但如果你按值查找,比如想知道“数字7在顺序表中的第几个位置”,那就只能老老实实从头遍历,挨个比较:
c复制int LocateElem(SeqList *L, ElemType e) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == e) {
return i + 1; // 返回位序
}
}
return 0; // 没找到
}
按值查找最好情况是第一个就命中,比较1次;最坏情况是最后一个才命中或者根本不存在,比较 n 次;平均比较次数是 (n+1)/2,时间复杂度 O(n)。这个地方特别容易被忽略,很多人背下了“顺序表随机访问O(1),插入删除O(n)”,却忘了按值查找同样是O(n)。如果后续学二分查找,你还会发现一个前提:顺序表必须有序,而且得能随机访问,两者缺一不可。
4. 可直接复制运行的顺序表演示工程
4.1 一个包含动态扩容的完整示例代码
上面零散讲了结构体、插入、删除、查找,这里我把整套东西拼成一个完整程序。为了不让你被文件拆分干扰,这里把所有代码写在一个 main.c 里。你直接复制到编译环境里就能跑,看输出你就明白每一步发生了什么。
c复制#include <stdio.h>
#include <stdlib.h>
#define INIT_CAPACITY 5
typedef int ElemType;
typedef struct {
ElemType *data;
int length;
int capacity;
} SeqList;
// 初始化
void InitSeqList(SeqList *L) {
L->data = (ElemType *)malloc(INIT_CAPACITY * sizeof(ElemType));
if (L->data == NULL) {
printf("内存分配失败\n");
exit(1);
}
L->length = 0;
L->capacity = INIT_CAPACITY;
}
// 扩容:容量扩大为原来的2倍
int ExpandSeqList(SeqList *L) {
int newCapacity = L->capacity * 2;
ElemType *newData = (ElemType *)malloc(newCapacity * sizeof(ElemType));
if (newData == NULL) {
printf("扩容失败\n");
return 0;
}
for (int i = 0; i < L->length; i++) {
newData[i] = L->data[i];
}
free(L->data);
L->data = newData;
L->capacity = newCapacity;
printf("容量不足,已扩容为 %d\n", L->capacity);
return 1;
}
// 插入
int SeqListInsert(SeqList *L, int pos, ElemType e) {
if (pos < 1 || pos > L->length + 1) {
return 0;
}
if (L->length == L->capacity) {
if (!ExpandSeqList(L)) return 0;
}
for (int i = L->length; i >= pos; i--) {
L->data[i] = L->data[i - 1];
}
L->data[pos - 1] = e;
L->length++;
return 1;
}
// 删除
int SeqListDelete(SeqList *L, int pos, ElemType *e) {
if (pos < 1 || pos > L->length) {
return 0;
}
*e = L->data[pos - 1];
for (int i = pos; i < L->length; i++) {
L->data[i - 1] = L->data[i];
}
L->length--;
return 1;
}
// 打印
void PrintSeqList(SeqList *L) {
printf("当前顺序表: [");
for (int i = 0; i < L->length; i++) {
printf("%d", L->data[i]);
if (i != L->length - 1) {
printf(", ");
}
}
printf("]\n");
}
// 销毁
void DestroySeqList(SeqList *L) {
free(L->data);
L->data = NULL;
L->length = 0;
L->capacity = 0;
}
int main() {
SeqList L;
InitSeqList(&L);
// 故意在容量只有5的初始状态下插入6个元素,触发扩容
SeqListInsert(&L, 1, 2);
SeqListInsert(&L, 2, 5);
SeqListInsert(&L, 3, 9);
SeqListInsert(&L, 4, 7);
SeqListInsert(&L, 5, 8);
PrintSeqList(&L);
// 第6个元素会触发扩容
SeqListInsert(&L, 3, 6);
PrintSeqList(&L);
ElemType deleted;
if (SeqListDelete(&L, 3, &deleted)) {
printf("删除了第3个位置的元素: %d\n", deleted);
}
PrintSeqList(&L);
DestroySeqList(&L);
return 0;
}
这段代码里我特意把初始容量设置成5,是为了让你能看到扩容发生的过程。平时自己练习设成10或100都行。需要注意:函数形参是 SeqList *L,调用时全是 &L。为什么不能直接传 L?因为C语言函数传参是值复制,如果直接传结构体变量,函数里改的是实参的一副本,length、capacity 的改动全带不回来,这是一个非常经典的错误。
4.2 运行输出和代码行为解读
上面代码编译运行后,会得到类似下面的输出:
text复制当前顺序表: [2, 5, 9, 7, 8]
容量不足,已扩容为 10
当前顺序表: [2, 5, 6, 9, 7, 8]
删除了第3个位置的元素: 6
当前顺序表: [2, 5, 9, 7, 8]
第一条输出对应前5次插入,全部在尾部追加,没有触发扩容。第二条中间的“容量不足,已扩容为 10”是第3个位置插入、且原表已满时触发的。这里有个容易误判的地方:第6个元素插入后,顺序表长度变成6,此时输出顺序表 [2, 5, 6, 9, 7, 8],恰好说明新元素6已经插入到了位序3,而不是追加到末尾。第三行删除6后,整个表又回到了接近初始状态的样子。你可以在 main 函数里随便改插入位置、删除位置,再观察输出的变化,比看任何教材上静态的图都直观。
判断你代码写得对不对,最笨也最有效的方法就是打印每一轮移动后的数组内容。把 SeqListInsert 循环里加一句 PrintSeqList(L),你能看到数据是如何一步步往后挪的。我已经帮你验证过这段程序的逻辑,但建议你亲手运行一遍,再试着把 for (int i = L->length; i >= pos; i--) 改成 for (int i = pos - 1; i <= L->length; i++),运行后观察结果,你就能从错误中真正理解方向的重要性。
5. 常见问题与排查技巧实录
5.1 传结构体指针还是传结构体本身
刚开始练习顺序表,最容易出现的编译或逻辑问题就是函数参数传错了。很多人写 SeqListInsert(L, 3, 6),当 L 是结构体变量时,这个调用语法没错,但是在函数内部修改 length 并不会作用到 main 里的 L 上。因为C语言函数传参是按值传递,形参是实参的一份拷贝,函数内部计算了半天,实际结构体一点没变。
必须写成 SeqListInsert(&L, 3, 6),用指针持有结构体的地址,函数内部才能通过 L->length++ 这种写法真正修改原变量。如果确实想传结构体本身作为值,你还可以让函数返回新的结构体,L = SeqListInsert(L, 3, 6),但它会反复拷贝整个结构体,效率很差,而且代码风格也不统一。写数据结构时统一用指针,养成肌肉记忆最好。
5.2 位序和下标混为一谈:1和0的恩怨
顺序表教科书里常把位置称为“位序”,从1开始算:第1个元素、第2个元素;而数组下标从0开始。于是就会出现这样的矛盾:要删除“第3个位置的元素”,下标其实是2。代码中 pos 代表位序,data 的访问必须写成 data[pos - 1],插入后 data[pos - 1] = e。
如果你把位序当下标用,最直接的问题是:插入到第1个位置时,数据被放到了 data[1],而不是 data[0],表头位置永远是空的;删除位置=length 时,data[length] = data[length+1] 直接越界,程序可能会崩也可能不崩,但这种不确定行为比崩了更可怕,往往在运行很久后才能暴露。排查时可以统一记忆一句话:用户眼里第1个元素,在内存里是 data[0],所有边界判断都用位序,所有内存操作用下标,中间通过 pos-1 桥接。
5.3 扩容的三种姿势:增量、倍增和一次到位
动态顺序表一定会遇到扩容问题。最简单的扩容方法不是 realloc,而是自己 malloc 一块新内存,把旧数据复制过去,再释放旧内存。为什么很多教材建议这么做而不是直接 realloc?因为 realloc 可能原地扩展,也可能搬去新地址,如果原地扩展失败返回NULL,你原来的指针还指着旧内存,盲目把返回值赋给原指针会丢指针;真要用 realloc,得用一个临时指针接返回值判断后再赋回去,稍不注意就埋坑。
扩容策略一般有“固定增量”和“倍增”两种。固定增量比如每次多增加10个位置,适合内存特别紧张的嵌入式场景;倍增比如每次扩大到原来的2倍,C++ vector 和很多动态数组库都采用这种方式,因为它能保证连续多次扩容的总时间开销接近O(n),均摊下来成本很低。实战推荐倍增方式,初始容量设小一点,等系统运行后再自动扩,避免一开始就申请一大块用不完的内存。
提供一段可用的扩容代码可以参考上文的 ExpandSeqList。里面有个必须注意的点:复制数据时要用旧 length 作为循环边界,而不是旧 capacity。因为旧数组后半部分可能全是垃圾数据,复制过来没有任何意义,还会浪费时间和新内存。
5.4 打印、判空、边界:最不起眼的坑
很多同学逻辑主体没问题,却在打印函数上翻车。打印顺序表时,循环条件是 i < L->length,写成 i <= L->length 就会多打印一个垃圾值,因为 data[length] 不在有效范围内。判断是否为空不该用 capacity,应该用 length == 0。初始 capacity 可能是10,但里面一个有效数据都没有,你拿 capacity 判断等于没判。
还有人对 pos < 1 || pos > L->length + 1 不理解,写成了 pos > L->length,这会导致无法在表尾追加元素;也有人在删除函数中允许 pos == length+1,导致删除一个不存在的位置。每次写边界条件时,建议在纸上画一个长度为3的顺序表,把合法范围写出来再下笔,比单纯背公式可靠得多。
6. 实际场景里如何选择顺序表和进一步发展
6.1 什么业务场景适合用顺序表“扛大梁”
顺序表并不是只能出现在考试卷上的数据结构,它在工程中的出场率其实相当高。凡是数据量不大、插入删除不频繁、但是经常按下标随机访问的场景,顺序表都是优先选择。例如一个程序需要记录最近100条操作日志,日志只追加且要随时打印,数组或顺序表足够;再比如实现一个键盘缓冲区、一个消息队列的底层存储,也大量依赖连续数组。
反过来,如果你写的是一个需要频繁在中间插入或删除节点的文本编辑器,比如光标处插入字符,顺序表的每次操作都要移动后续全部字符,性能会肉眼可见地拉胯。这也是为什么许多编辑器内核采用分段数组或链表来管理文本缓冲区。这里得到一个实践判断准则:看你的程序是“读多写少”还是“写多读少”,前者放心用顺序表,后者要多考虑链表或更复杂的数据结构。
6.2 顺序表后续还能怎么加深
这篇只讲了最基础的动态顺序表,后续你可以从这几个方向继续扩展:一是把 ElemType 改成结构体,实现一个简单的学生成绩管理或图书信息管理系统,这会让你真正理解为什么数据结构和实际业务是紧密绑定的;二是在顺序表上实现排序、二分查找、去重、合并等算法,它们在数组基础上才有更高效的写法;第三是尝试写一个静态顺序表版本,把容量直接用宏定义固定,对比动态版本在代码复杂度上的差异,你会对 C语言的内存管理有更深的认识。
当然,顺序表再往后延伸就是链表、栈、队列和树。你会发现,前面这些通过下标搬数据的练习,会在链表那一章变成通过指针穿针引线,思维又得转一个弯。数据结构的学习就是这样,很多操作的图解法看上去很轻松,但只有自己一行行写过、调试过,才能把图上的箭头变成代码里的移动步骤。
在我带过的初学者里,卡在顺序表这一个知识点上的人非常少,但如果卡住,基本都是因为跳过了动手画图这一步。这篇文章里我用 ASCII 图和表格模拟了图解析,建议你自己在纸上也画一遍插入过程,给每一个元素编号,然后照着写代码。画完这遍,后面链表节点怎么指向,你会顺利很多。
