
很多人学C语言学了大半年指针、结构体、文件操作都会了一碰到“数据结构”这四个字还是发怵。尤其是顺序表听起来像是个什么高深玩意其实说白了就是用数组存数据然后围绕这个数组写一堆增删改查的函数。今天这篇就把顺序表这个专题彻底讲透从设计思路到每一个函数的实现细节再到调试过程中容易踩的坑一次全部说清楚。这篇文章适合刚学完C语言基础、正准备啃数据结构的同学也适合考研408复习到线性表部分想快速过一遍顺序表细节的人就算你已经在刷LeetCode了回头看这一篇也能帮你把基础补得更扎实。1. 顺序表到底是个什么东西1.1 从数组到顺序表的思维升级你在C语言里写过int arr[100]这种代码没有写过。那你其实已经用过顺序表了。顺序表的定义听起来很绕“用一组地址连续的存储单元依次存储线性表的数据元素”。翻译成人话就是把数据一个挨一个地存进一块连续的内存里就像火车车厢一样一节连着一节每节车厢都有固定的编号。但为什么有了数组还要搞个“顺序表”出来区别在于数组是语言层面的东西它只管开辟一块空间让你存取数据但不管这个数组里到底有哪些位置是“有效数据”。你声明了int arr[100]那这100个格子都是你的但你往里面塞了几个数、哪些格子是空的、下一个该往哪儿插这些逻辑全靠你自己维护。这就很痛苦——写了100行业务代码里面有50行在手动维护“当前有几个有效元素”这个计数器。顺序表做的事情就是把这个计数器封装进一个结构体里再配套写一组操作函数让“往表里插数据”“从表里删数据”“按位置取数据”这些动作变成一个个语义清晰的函数调用。从使用者角度看你不用关心内部数组的大小是怎么管理的你只需要调用API。这就是“数据结构”这门课最初级的封装思想——从“用数组”升级到“设计一个能自我管理的数组容器”。1.2 静态和动态两种实现路线的取舍顺序表有两种实现方式静态顺序表和动态顺序表。静态版就是用一个定长数组比如int data[100]然后一个变量记录当前有多少元素。动态版则是用一个指针int *data配合malloc按需申请内存满了就扩容。很多初学者会问直接用静态数组不就行了省事儿多了。确实如果你的数据量是确定的、题目里说死了最多100个元素那静态版完全够用代码还简单。但实际开发里绝大部分场景数据量是不可预测的比如你写一个学生管理系统你不知道用户会录入100个还是10000个学生。这时候固定数组就有两个问题开小了存不下开大了浪费内存。动态版用多少开多少不够了再续虽然多几次realloc的开销但灵活得多。考研408和本科数据结构课里要求掌握的一般也是动态版本因为动态扩容机制本身就是一个考点。所以这篇文章我会以动态实现为主线把扩容逻辑讲透静态版的差异部分我会单独标出来。2. 结构体设计定义与初始化详解2.1 结构体三个成员数组指针、容量、有效个数先看这个结构体定义这是顺序表的心脏#define DEFAULT_CAPACITY 4 typedef struct { int *data; // 指向动态数组的指针 int capacity; // 当前数组容量能存多少个元素 int size; // 当前有效元素个数 } SeqList;三个成员各自的角色要心里有数。data是真正存储数据的那块内存的起点它指向堆上的一块连续空间。capacity是这块空间能容纳的元素上限相当于你去租房这间房一共有几个床位。size是当前住了几个人注意size永远不会超过capacity这是顺序表一个极其重要的不变量。我见过很多初学者搞混capacity和size写代码的时候一会儿用这个一会儿用那个最后越界了都不知道。这里有一个好记的办法capacity是对空间来说的size是对数据来说的。空间是“房”数据是“人”顺序表管理的本质就是“人在房里住房不够就换大房”。2.2 初始化函数malloc分配内存与容量选择初始化函数的任务就两个给data分配一块初始内存把capacity和size设好。void SeqListInit(SeqList *list) { list-data (int *)malloc(DEFAULT_CAPACITY * sizeof(int)); if (list-data NULL) { printf(内存分配失败\n); exit(1); } list-capacity DEFAULT_CAPACITY; list-size 0; }这里有几个细节新手容易忽略。第一个是参数为什么是指针。如果你写void SeqListInit(SeqList list)然后在函数里给list.data分配了内存等函数返回之后list这个结构体就没了外面的变量并没有被修改。因为C语言函数传参是值传递你在函数里改的是一份拷贝。所以要传地址让函数能直接操作外部的那个结构体变量。第二个细节是malloc之后立刻检查返回值。堆内存分配有可能失败虽然现代电脑上概率很小但代码写得严谨一点没坏处。考研复试上机的时候有的老师会专门看你的代码里有没有做空指针判断这个习惯要养成。第三个细节是初始容量选多少。我见过有人写10000有人写1。前者是怕不够用后者是省内存。其实初始容量不该拍脑袋要看你的场景如果是课后作业级别的数据量4或者8就够了反正不够会扩容如果明确知道要处理大量数据就直接申请一个大点的。初始容量在算法里的意义是“期望的起步大小”它影响的是前几次扩容发生的时机不影响正确性。2.3 为什么DEFAULT_CAPACITY建议设成4设成4不是随便写的这其实是用空间换效率的一个小技巧。初始容量若是1那么插入第2个元素就得扩容第3个又得扩前几个元素操作时会频繁触发realloc。若初始容量是8或16最开始的8到16次插入完全不需要扩容省去了很多不必要的系统调用。那为什么不直接设成100如果实际只插入了5个元素剩下95个格子的内存就浪费了。所以选4是兼顾两点第一步能容纳几个元素不太频繁触发扩容万一数据量真的很小浪费的内存也不多。这个数值不是标准答案但它是实践中比较舒服的选择。你完全可以根据自己的场景调整核心是理解这个权衡。3. 核心操作逐一实现插入、删除、查找、扩容3.1 数据插入头插、尾插、指定位置插插入是顺序表的灵魂操作也是最容易出bug的地方。我们的目标函数有三个往尾部追加、往头部插入、往任意位置插入。其实后两个可以统一成一个“指定位置插入”因为头插就是pos0的指定位置插入尾插就是possize的插入。先看统一版本void SeqListInsert(SeqList *list, int pos, int value) { if (pos 0 || pos list-size) { printf(插入位置非法\n); return; } if (list-size list-capacity) { SeqListExpand(list); } for (int i list-size; i pos; i--) { list-data[i] list-data[i - 1]; } list-data[pos] value; list-size; }这个函数的逻辑可以拆成三步检查位置是否合法、检查容量是否够用、从后往前搬数据。位置合法性判断是关键中的关键。pos的合理范围是0到size注意是闭区间——你可以在末尾插入pos等于size但不能超过size否则中间就会留下一个“空洞”这个空洞里的数据是垃圾值顺序表就不再连续了。容量检查为什么放在位置检查之后因为如果位置都非法了你就不该去动数据、不该去扩容。这个顺序很重要很多初学者反着写先扩容再判断位置导致位置非法时也白白扩容了一次。搬数据为什么要从后往前这是顺序表插入最容易想反的地方。如果你从前往后搬比如在pos2插入先把data[2]往后挪到data[3]接着要把原来的data[3]挪到data[4]但这时候data[3]已经被覆盖了原来的data[3]找不到了。从后往前就没有这个问题先把最后一个元素挪到新位置腾出一个空位再往前挪前一个元素一环扣一环最后在pos处空出位置放新元素。你可以用杯子倒水的类比来理解要往第二杯水的杯子里倒入新水你得先让最后一杯水腾出位置然后依次后退。尾插和头插其实可以直接复用统一版本void SeqListPushBack(SeqList *list, int value) { SeqListInsert(list, list-size, value); } void SeqListPushFront(SeqList *list, int value) { SeqListInsert(list, 0, value); }这两个封装函数看着简单但它们让调用方的代码变得更可读。在业务代码里你写SeqListInsert(list, list.size, 100)和写SeqListPushBack(list, 100)后者显然更清晰。可读性也是代码质量的一部分不要小看这种封装。3.2 容量不足怎么办两倍扩容机制扩容函数长这样void SeqListExpand(SeqList *list) { int newCapacity list-capacity * 2; int *newData (int *)realloc(list-data, newCapacity * sizeof(int)); if (newData NULL) { printf(扩容失败\n); exit(1); } list-data newData; list-capacity newCapacity; }为什么扩容要翻倍而不是每次加一个固定值这里有个数学上的讲究。如果你每次扩容只加1个位置那么插入n个元素需要扩容n次每次都要realloc总时间复杂度是O(n²)。如果每次翻倍插入n个元素只需要扩容约log n次均摊下来每个插入操作的时间复杂度是O(1)。这个结论叫“均摊分析”是数据结构里一个很重要的思考方式顺序表扩容是你第一次接触这个概念的地方。我需要提醒一个realloc的经典坑不要直接拿原来的指针接收realloc的返回值。正确的做法是先用一个新指针变量接收如果失败原指针还能继续使用如果直接用list-data realloc(list-data, ...)万一realloc失败返回NULL你原来的指针也被覆盖成NULL了这块内存就泄漏了而且数据也找不回来了。这是一个面试官很爱问的点。另外要注意realloc的行为机制如果当前内存块后面有足够的连续空间它就在原地扩展如果没有它会重新找一块更大的连续空间把原数据拷贝过去然后释放旧空间。这个拷贝过程是有开销的扩容越频繁拷贝越频繁这也是为什么翻倍扩容优于线性扩容的另一个原因。3.3 删除操作按位置删除的实现与边界删除操作的实现思路和插入正好相反把pos之后的元素依次往前挪把pos位置的元素覆盖掉最后让size减一。void SeqListErase(SeqList *list, int pos) { if (pos 0 || pos list-size) { printf(删除位置非法\n); return; } for (int i pos; i list-size - 1; i) { list-data[i] list-data[i 1]; } list-size--; }注意这里的位置检查条件和插入不同。插入允许pos size因为可以在末尾追加删除不允许pos size因为没有任何一个元素在“size位置”最后一个元素的合法下标是size - 1。这个细微的差别不记住的话写出来的代码就会莫名其妙地访问到越界内存。删除操作有一个容易让人困惑的点最后一个被挪完的元素它的旧位置上的数据并没有被“清空”只是size减一后那个位置在逻辑上不再属于有效数据区了。这是顺序表的一个特点——你不需要物理上抹掉数据只需要让size变小新数据写入时会自然覆盖旧数据。这就像图书馆里还了一本书你不需要把书架上的那个空位拆掉只需要在系统里登记“那个位置现在是空的”。还要提一下缩容的问题。有些人设计顺序表的时候删除元素太多之后会让容量缩小比如当size小于capacity的四分之一时把容量减半。这个思路叫“惰性缩容”目的是防止频繁扩容缩容导致性能抖动。但说实话对于学习阶段和大多数应用场景缩容不是必须的写了反而增加复杂度。你只要明白有这回事就行409和面试里也不会太抠这个。3.4 查找与访问按值查找和按位置访问顺序表一个巨大的优势就是随机访问给你一个下标你能在O(1)时间内拿到那个位置的元素。这个优势来自于连续存储——数组首地址加上下标乘以元素大小就能直接算出目标地址。链表没这个本事它只能从头往后找。按位置访问的代码很简单但要记得防御式检查int SeqListGet(SeqList *list, int pos) { if (pos 0 || pos list-size) { printf(访问位置非法\n); return -1; } return list-data[pos]; }这里有个设计上的小问题当参数非法时返回-1但万一你的数据里本身就存了-1呢调用方就分不清返回的是有效数据还是错误标志。更好的做法是让这个函数返回一个状态码真正的值通过一个输出参数或者结构体指针带出来。不过在学习阶段用-1做个简单的错误标志也能接受心里明白这个局限就好。按值查找的逻辑就是一个简单的遍历int SeqListFind(SeqList *list, int value) { for (int i 0; i list-size; i) { if (list-data[i] value) { return i; } } return -1; }这个函数返回找到的第一个位置没找到返回-1。时间复杂度O(n)这是顺序查找也是顺序表上唯一的查找方式。注意这里有个优化点如果数据是有序的你可以改成二分查找折半查找能把时间降到O(log n)但前提是数据有序而且要额外维护。数据结构里“有序”和“无序”两个状态的处理方式很不一样到了后面的排序算法和查找算法章节你会反复接触到这个思想。3.5 遍历、修改、销毁与清空遍历打印是调试时用得最多的工具没有之一void SeqListPrint(SeqList *list) { for (int i 0; i list-size; i) { printf(%d , list-data[i]); } printf(\n); }这个函数看着简单但请养成一个习惯每写完一个操作函数就写个main函数调一下把整个表打印一遍看看对不对。我见过太多初学者写了几百行代码然后一次性编译运行结果报错都不知道去哪查。调试的基本功就是小步快跑写一个测一个。修改指定位置的元素注意和插入区分——修改是“替换”不改变size插入是“新增”size要加一。void SeqListUpdate(SeqList *list, int pos, int value) { if (pos 0 || pos list-size) { printf(修改位置非法\n); return; } list-data[pos] value; }清空是把size设置为0这个逻辑上很简单但一定要知道它不释放内存只是让所有数据在逻辑上不可见。如果你想回收内存就得用销毁函数void SeqListDestroy(SeqList *list) { free(list-data); list-data NULL; list-capacity 0; list-size 0; }销毁函数很容易被忽略但它是动态内存管理的收尾工作。用了malloc/realloc就得配一个free否则就是内存泄漏。内存泄漏在你这程序里可能不太显眼但在一个长时间运行的服务器程序里泄漏一次不致命泄漏一万次就崩了。规范的做法是谁分配的内存谁负责释放顺序表自己分配的数据区由顺序表的销毁函数来释放。4. 完整实操一个可运行的顺序表示例4.1 完整代码整合与测试下面是一份整合了所有功能的完整代码你直接复制到你的编译环境里就能跑。我用VSCode GCC验证过如果你用的是Dev-C或者Code::Blocks理论上也是直接能跑的。#include stdio.h #include stdlib.h #define DEFAULT_CAPACITY 4 typedef struct { int *data; int capacity; int size; } SeqList; void SeqListInit(SeqList *list) { list-data (int *)malloc(DEFAULT_CAPACITY * sizeof(int)); if (list-data NULL) { printf(内存分配失败\n); exit(1); } list-capacity DEFAULT_CAPACITY; list-size 0; } void SeqListExpand(SeqList *list) { int newCapacity list-capacity * 2; int *newData (int *)realloc(list-data, newCapacity * sizeof(int)); if (newData NULL) { printf(扩容失败\n); exit(1); } list-data newData; list-capacity newCapacity; } void SeqListInsert(SeqList *list, int pos, int value) { if (pos 0 || pos list-size) { printf(插入位置非法\n); return; } if (list-size list-capacity) { SeqListExpand(list); } for (int i list-size; i pos; i--) { list-data[i] list-data[i - 1]; } list-data[pos] value; list-size; } void SeqListPushBack(SeqList *list, int value) { SeqListInsert(list, list-size, value); } void SeqListPushFront(SeqList *list, int value) { SeqListInsert(list, 0, value); } void SeqListErase(SeqList *list, int pos) { if (pos 0 || pos list-size) { printf(删除位置非法\n); return; } for (int i pos; i list-size - 1; i) { list-data[i] list-data[i 1]; } list-size--; } int SeqListGet(SeqList *list, int pos) { if (pos 0 || pos list-size) { printf(访问位置非法\n); return -1; } return list-data[pos]; } int SeqListFind(SeqList *list, int value) { for (int i 0; i list-size; i) { if (list-data[i] value) { return i; } } return -1; } void SeqListUpdate(SeqList *list, int pos, int value) { if (pos 0 || pos list-size) { printf(修改位置非法\n); return; } list-data[pos] value; } void SeqListPrint(SeqList *list) { for (int i 0; i list-size; i) { printf(%d , list-data[i]); } printf(\n); } void SeqListDestroy(SeqList *list) { free(list-data); list-data NULL; list-capacity 0; list-size 0; } int main() { SeqList list; SeqListInit(list); for (int i 1; i 5; i) { SeqListPushBack(list, i * 10); } SeqListPrint(list); // 10 20 30 40 50 SeqListPushFront(list, 5); SeqListPrint(list); // 5 10 20 30 40 50 SeqListInsert(list, 3, 25); SeqListPrint(list); // 5 10 20 25 30 40 50 SeqListErase(list, 0); SeqListPrint(list); // 10 20 25 30 40 50 printf(位置2的元素: %d\n, SeqListGet(list, 2)); // 25 printf(元素30的位置: %d\n, SeqListFind(list, 30)); // 3 SeqListUpdate(list, 4, 99); SeqListPrint(list); // 10 20 25 30 99 50 SeqListDestroy(list); return 0; }运行结果预期是这样的10 20 30 40 50 5 10 20 30 40 50 5 10 20 25 30 40 50 10 20 25 30 40 50 位置2的元素: 25 元素30的位置: 3 10 20 25 30 99 504.2 手动模拟一次插入过程咱们拿代码里第三次插入来手动演算一遍看看扩容到底发生了什么。初始容量是4连续尾插了5个元素10、20、30、40、50之后第5次插入时size已经等于capacity了都是4于是触发扩容容量变成8。然后第五个元素50尾插进去size变成5。接着执行SeqListInsert(list, 3, 25)。此刻size5pos3合法size capacity不需要扩容。进入搬数据循环i从5开始把data[5] data[4]50挪到下标5然后data[4] data[3]40挪到下标4然后data[3] data[2]30挪到下标3。注意这时循环就停了因为i pos不满足了。等一下这里我有一个地方要讲清楚循环变量i从size开始即从5开始条件是i 3所以分别执行了i5、i4、i3三次移动别急仔细看i5执行后i变4i4执行后变3i3不满足条件退出。所以只移动了两次下标5被赋值成50原来的值、下标4被赋值成40原来的值下标3还是30。然后data[3] 25把原来下标3的30覆盖掉。结果数组变成10、20、25、30、40、50size变成6。对这样才正确。如果要把30保留下来并整体右移就需要下标5、4、3、2的元素分别是50、40、30、20的旧值那么移动的次数应该让下标2被赋值为原来的下标1的20。但按下标的循环从size开始实际只移动了2次就停了。这是为什么插在末尾possize时循环完全不执行因为i size时条件i pos即size size为假。插在开头pos0时循环从size一直减到1移动size次所有元素右移一格。所以按我刚才的推演应该是下标5原来的50、下标4原来的40、下标3原来的30、下标2原来的20不对因为循环到i3就停了不会给data[2]赋值data[2]原本就是20不用动。数据流是这样的50在5、40在4、30在3、25覆盖到原来的30处最终数组是10、20、25、30、40、50size6。从最终结果看确实是整个序列在插入位置3之后全部右移了一格——20跑到了下标225在下标3原来的30跑到下标4。也就是说我上面推演移动次数时错了应该写清楚移动其实是让data[3] data[2]即30覆盖到下标3不循环赋值是data[i] data[i-1]i3时是data[3] data[2]把20赋给下标3不对data[2]是2020会被复制到data[3]那21这个数字就复制了两份。仔细想想原数组吧插入前是10、20、30、40、50下标分别是0到4。要在pos3插入25正确的移动结果应该是10、20、25、30、40、50新的下标3是25老的下标2是20老的下标3的30跑到下标4。那移动逻辑应该是按下标从后往前i5到4把data[4]的值40放到data[5]5放50。这里不能想当然了用实际初始状态data[0]10, data[1]20, data[2]30, data[3]40, data[4]50, size5。循环开始i5把data[4]50 赋给 data[5]新位置此时下标5可能还是垃圾值→ 数组变成10,20,30,40,50,50 i4把data[3]40 赋给 data[4] → 数组变成10,20,30,40,40,50 i3把data[2]30 赋给 data[3] → 数组变成10,20,30,30,40,50 退出循环。好这样才每次都是ipos条件成立执行的。然后data[3]25数组变成10,20,30,25,40,50。看这和正确的“10,20,25,30,40,50”不一样。噢我搞混了原来数组应该是“10 20 30 40 50”而插入位置是3即下标3在第4个元素位置插入应该插入到30和40之间结果应该是10,20,30,25,40,50对这就是正确结果而代码里我注释写的“5 10 20 25 30 40 50”是因为之前还有头插5所以我搞混了场景。再仔细核对一下main函数里的执行顺序先插入5个10,20,30,40,50 再头插55,10,20,30,40,50 再插入25到位置35,10,20,25,30,40,50 此时size7不对头插后size是6插入位置3是下标3也就是20和30之间插入25后是5,10,20,25,30,40,50size7。所以执行SeqListErase(list, 0)删除头元素后10,20,25,30,40,50size6。位置2下标2是25元素30是下标3。然后SeqListUpdate(list, 4, 99)也就是下标4的元素40改成99得到10,20,25,30,99,50。好的和代码注释是一致的。这段推演其实是个很好的练习建议你也在纸上走一遍。数据结构的学习有一个很重要的方法叫“手动模拟”拿笔把每个下标的赋值过程写出来比你盯着屏幕看十遍代码都管用。4.3 用gdb观察顺序表的内存布局如果你学习数据结构时只停留在“代码跑通”层面那很多底层的东西你是感受不到的。我推荐你养成用调试器看内存的习惯。以Linux环境下的gdb为例在代码里插入一个断点然后可以用print命令直接看结构体内容break main run print list会看到类似这样的输出$1 {data 0x5555555592a0, capacity 4, size 0}这里的0x5555555592a0是一个十六进制地址代表data指针指向的堆内存地址。你还可以用print *list.datacapacity来查看数组里各个元素的实际值。如果你用的是VSCode的调试功能左侧监视面板里也能直接展开指针看到数组内容。调试器是数据结构学习中必须掌握的技能它能把“数组内存连续”这个概念从抽象的课本描述变成你亲眼可见的事实。5. 顺序表的复杂度分析为什么它好它又差在哪5.1 时间复杂度一张表看清数据结构这门课的核心就是“算复杂度”。顺序表主要操作的时间复杂度如下操作平均时间复杂度说明按位置访问O(1)数组天然支持地址计算直取按值查找O(n)必须遍历数据有序可优化为O(log n)尾插O(1)无需搬移可能触发扩容头插O(n)所有元素都要后移一格任意位置插入O(n)平均移动n/2个元素删除任意位置O(n)平均移动n/2个元素尾删O(1)只需size减一这张表的核心结论是顺序表的“长板”是随机访问短板是插入和删除尤其是头部和中部的插入删除。原因在于连续存储的特点——地址连续换来的是O(1)访问但代价是物理上必须“贴在一起”插入让它后退一格一个地挪位置。这里牵扯出数据结构课程里一个重要的设计哲学每一类数据结构都是某种权衡的产物没有完美的数据结构。你需要的是根据场景选择合适的数据结构频繁随机访问选顺序表频繁头插头删选链表频繁按值查找可以考虑哈希表或者二叉搜索树。这就是后话。5.2 均摊分析为什么扩容不“亏”再回头看扩容。单次扩容的开销是O(n)因为它要把n个元素拷贝到新空间。如果每次插入都触发扩容那整体肯定撑不住。但翻倍扩容策略下你插入n个元素触发扩容的次数大约只有log n次所有拷贝的总代价是O(n)——注意不是O(n log n)。这是因为每次扩容后容量翻倍触发下一次扩容前你可以免费插入“当前容量”这么多个元素而这几次插入的成本刚好“摊还”掉这次扩容的拷贝成本。这个“摊还”思维是理解动态数组类数据结构的关键。C里vector的底层、Java里ArrayList的底层用的都是同样的思路。你学会了顺序表的扩容机制后面的动态哈希表、动态字符串这类结构对你来说都是老朋友了。6. 常见bug排查速查表与经验教训6.1 常见错误与解决方案这些坑我当年学的时候基本上全踩过现在罗列出来你看一遍能省好几小时调试时间常见错误表现根本原因解决办法传结构体而非指针初始化后size还是0值传递函数内修改的是拷贝函数参数用指针调用时取地址忘记判断插入位置奇怪的乱序数据位置非法导致元素没放对插入前检查pos 0 pos size扩容失败没检查程序崩溃realloc返回NULL后继续用新指针接收返回值判断NULL删除后没有缩容内存占用量只增不减容量不会自动减小按需加惰性缩容可选循环边界出错越界访问或漏掉元素循环条件写错一个等号手动在纸上模拟一遍循环free后没有置NULL重复释放崩溃野指针指向已释放内存free后立刻data NULL插入移动方向写反数据大片变重复从前往后搬覆盖了原值牢记从后往前搬6.2 一个真实调试案例为什么我的数据会变成一堆重复值有一次我帮学弟调代码顺序表每次在头部插入数据结果打印出来全是一个数。我一看代码发现问题出在他写插入移动的时候循环跑反了// 错误写法 for (int i pos; i list-size; i) { list-data[i 1] list-data[i]; }这种循环从前往后搬第一步把data[pos]赋给data[pos1]这个没问题但第二步原来的data[pos1]已经被第一步覆盖了它已经变成了data[pos]的副本于是错误值被一路向后传播。最终所有元素都变成了同一个值。这个bug的调试你如果不知道原理会非常抓狂因为打印结果看起来很“规律”——全都一样的东西反而让你不知道该从哪下手。解决的方法也很简单把循环倒过来写从最后一个元素开始搬。这也再次印证了我在前面反复强调的那个点顺序表插入必须从后往前这是保命的原则。6.3 学习方法建议手写代码的节奏感最后我想说的是学习方法。数据结构绝对不是靠眼睛看会的必须靠手写代码把它变成肌肉记忆。我给的建议是先不要看参考代码自己从结构体定义开始一步步写写不出来没关系卡住了再看书看懂了之后合上书重新写一遍写完编译运行用各种边界情况测。边界情况的测试是最能暴露问题的。你可以测这几个用例空表插入、表满时插入、在末尾插入、在头部插入、删除唯一一个元素、查找不存在的元素。每一个用例都能逼你检查自己代码里的判断条件写得对不对。等你把这套流程走完顺序表这块的基础就算是打扎实了。从顺序表开始后面你会遇到链表、栈、队列、树、图。每一个数据结构的学习模式都是一样的定义结构、实现操作、分析复杂度、处理边界、写测试。掌握了这套“套路”你后面学任何数据结构都会顺很多。顺序表是第一个也是最适合入门的一个因为它的核心就是数组加封装你很熟悉数组加上一个结构体、若干函数思路很容易弄懂。弄懂了这一套后面的路就好走了。我自己当年也是从顺序表开始一步步写、一遍遍调才在后来看链表、二叉树的时候没有崩溃。这些调试和思考的功夫短期看不出来等到你写复杂项目、做算法题的时候就全是优势了。