
简介这份资源为头歌平台数据结构实训中顺序表基本操作1-6关的完整通关代码与文档面向正在学习线性表、需要完成实训作业或备考数据结构的高校学生。内容覆盖插入、删除、按序号查找、按值查找、逆置及两个有序表合并六大核心操作以C语言给出可运行实现每关代码段均含填空压轴位置能够帮助读者对照理解顺序表存储结构与算法逻辑。资源包内共1个docx文件文件大小约54KB文档中按关卡整理源码与简要说明便于复制调试与复习整理。该资源在CSDN已有16045人浏览学习是经过较多学习者验证的实训参考读者拿到后可直接查看关键函数实现再结合编辑器运行测试遇到“位置不合法”或“空间不足”等常见报错也能通过代码注释快速定位。整体适合入门顺序表操作、课堂实验自查及期末考前快速回顾。1. 顺序表六关练的是内存里挪数据的手感顺序表这块入门者最大的误解是“我会数组就会顺序表”。真到“头歌数据结构顺序表的基本操作1-6关”这类实训里六关全套走下来会发现考点不是数组语法而是“插入元素先移动哪个位置”“删除后 length 值变不变”“边界到底写成 还是 ”这些细节。第1关建表、第2关插入、第3关删除、第4关取值/查找、第5关遍历/清空、第6关综合操作正好把顺序表的基本操作按难度排成一道完整阶梯。这个系列适合刚学数据结构的人把原理落成代码也适合准备机考或手撕代码的人找回手感。本文不假设你见过原题只按最常见的关卡对象把它们逐个过一遍给出实现、边界检查和坑位。2. 动手前先把底子打好结构体定义与六关的通用约定2.1 为什么用结构体包数组一个 length 字段解决“表里到底有几个元素”顺序表的内存布局是连续的逻辑上相邻的元素物理地址也相邻。优点是按下标访问是 O(1)代价是插入和删除要带动后面的元素整体搬家。C 语言里如果单独拿数组用长度总是靠额外变量维护函数传参时这个变量很容易丢掉。所以教材和在线实训平台都约定用结构体把数组和长度包在一起函数之间传递时只传这个结构体长度信息就不会丢。#include stdio.h #include stdlib.h #define MAXSIZE 100 // 静态数组版顺序表 typedef struct { int data[MAXSIZE]; // 存放元素 int length; // 当前元素个数初始为 0 } SqList;这里 data 是静态数组容量写死为 100。内存分配在栈上简单直接适合第一二关先把逻辑跑通。初始化函数要做的就是把 length 置 0。这一步看着多余实际是后续一切操作的前提如果 length 不置 0打印和查找都会读到随机值。另一种常见定义是动态分配用 int *data 指向 malloc 出来的空间结构体里多一个 maxSize 记录容量。动态分配的好处是容量在初始化时由参数指定而不是写死在代码里。很多综合性更强的关卡会给出这种结构体typedef struct { int *data; // 指向动态分配的内存 int length; // 当前元素数 int maxSize; // 总容量 } SqList;这两种定义在写插入、删除、查找时逻辑完全一致唯一差别在“表满”的判断静态写法判断 length MAXSIZE动态写法判断 length maxSize。后面所有代码我按动态版写因为它更能体现顺序表“连续存储 容量管理”的真实场景也更容易迁移到链表实验。2.2 位序从 1 开始、下标从 0 开始六关里最容易乱的换算顺序表统一把“第一个元素”称为第 1 个位序用 i 表示而数组下标从 0 开始。所以位序 i 对应的下标永远是 i - 1。六关里大量错误都出在这个换算上插入时循环从 length-1 倒着搬到 i-1删除时循环从 i-1 正着搬到 length-1取元素时返回 data[i-1]。建议在草稿纸上把“位序 → 下标”这个换算写下来再代入循环。平台判题通常只看你的函数被一组固定用例调用后的输出所以函数原型里有没有返回状态、参数是不是指针直接决定代码怎么写。常见两种原型如下// 带状态返回的版本成功返回 1失败返回 0 int ListInsert(SqList *L, int i, int e); // 有的关卡给的是 void失败时自行处理或直接忽略 void ListInsert(SqList *L, int i, int e);我没看到具体题面时一律按“返回 int、用指针传 L”来写因为这种写法最通用。如果平台给的是 void只需把返回类型改掉、去掉 return内部逻辑不需要动。2.3 一个最简测试框架先跑通初始化再谈其他不管第几关代码都是一个骨架声明表、初始化、调用操作、按格式输出。本地建一个 main 放在同一文件里能保证每次改完立刻看到结果int main() { SqList L; int ret; // 初始化容量为 10 个 int ret InitList(L, 10); if (ret 0) { printf(初始化失败\n); return 1; } // 插入三个元素最后表变为 10 15 20 ListInsert(L, 1, 10); ListInsert(L, 2, 20); ListInsert(L, 2, 15); ListTraverse(L); // 期望输出10 15 20 return 0; }注意 InitList 收 SqList* 是因为要修改结构体内部的 length 和 dataListTraverse 收 SqList 本身即可因为只读不写。这个传参习惯贯穿全部六关修改表内容的函数用指针只读表内容的函数用值避免无意中改掉原表。3. 前三关初始化、插入、删除——移动元素的三个方向3.1 第1关顺序表初始化malloc 后必须判空length 必须归零初始化是所有关卡的起点。第1关通常不会只让你“把 length 置 0”还会要求动态分配并记录容量。下面按动态分配版写兼容性更好// 动态分配方式初始化capacity 为申请的元素容量 int InitList(SqList *L, int capacity) { // 容量非法时直接失败 if (capacity 0) return 0; L-data (int *)malloc(sizeof(int) * capacity); // malloc 必须判空内存不足时返回失败 if (L-data NULL) return 0; L-length 0; // 空表长度为 0 L-maxSize capacity; // 记录总容量 return 1; }malloc 返回后第一件事必须是判空。很多人本地跑不判空也能过因为堆上内存充足但平台测试可能构造超大容量或耗尽内存漏判就会段错误而且这种段错误在本地极难复现是最典型的“翻车现场”。把 L-data NULL 这一行养成习惯初始化就稳了一半。如果平台给的是静态数组结构体初始化更简单只写 L-length 0 即可。这时真正的坑反而是 main 里忘了调用初始化就直接插入length 是随机值后续所有操作结果都是乱的。判断自己有没有踩这个坑可以在 main 里 printf(%d, L.length) 看一眼如果是 0 再继续。参数说明capacity 是申请的元素个数sizeof(int) * capacity 可能整型溢出如果 capacity 来自外部输入先做合法性检查返回值供调用方判断失败原因平台也可能用它比对函数是否按要求返回。3.2 第2关顺序表插入从最后一个元素开始往后搬循环别写成正向插入做的事只有一件把第 i 个位置腾出来再把 e 放进去。腾位置必须从表的末尾开始一个一个往后移否则前面的元素会把后面的覆盖掉。这是全关卡里最容易出错的一个循环。// 在顺序表 L 的第 i 个位置插入元素 ei 从 1 开始 int ListInsert(SqList *L, int i, int e) { int j; // 位置检查i 必须在 [1, length1] 之间 if (i 1 || i L-length 1) return 0; // 容量检查表满无法插入 if (L-length L-maxSize) return 0; // 从最后一个元素开始逐个后移腾出第 i 个位置 for (j L-length - 1; j i - 1; j--) { L-data[j 1] L-data[j]; } L-data[i - 1] e; // 新元素放入空位 L-length; // 长度加 1 return 1; }两个边界条件要背熟i 最大值是 length 1表示插到表尾之后。比如表里已有 3 个元素插到第 4 个位置是合法的i 最小值是 1不存在第 0 个位置。for 循环中 j 从 length-1 开始一直移到 i-1循环结束后下标 i-1 处正好是空位。常见错误是把 for 写成 for (j 0; j L-length; j)然后 data[j1] data[j]结果从前往后整个表被同一个值填满。判断方向有个快速办法插入后表里出现大量重复的第一个元素说明正向覆盖了如果最后一个元素出现在不该在的位置多半是边界差了 1。参数说明e 是待插入的元素值i 是位序不是下标返回 0 时调用方应知道是位置越界还是表满很多教材在这时会报错退出但实训题的判题只看返回值是否符合预期。3.3 第3关顺序表删除从被删位置开始向前搬length 记得减一删除比插入少一个容量检查但多一个“被删元素要能带出来”的要求。平台通常让你把被删元素通过指针返回或者至少把 length 减对。搬移方向与插入相反从被删位置的下一个元素开始一个一个往前挪。// 删除顺序表 L 的第 i 个元素被删元素存入 *e int ListDelete(SqList *L, int i, int *e) { int j; // 位置检查i 必须在 [1, length] 之间 if (i 1 || i L-length) return 0; *e L-data[i - 1]; // 先取出被删元素 // 从 i 位置开始把后面的元素整体前移 for (j i - 1; j L-length - 1; j) { L-data[j] L-data[j 1]; } L-length--; // 长度减 1 return 1; }被删元素取出后下标 i-1 处就逻辑上空了出来接下来的 for 从这个位置开始前移。循环里 j 的最大值是 length-2因为最后一个元素搬到倒数第二个位置后原来的最后一位就不用管了后续操作会覆盖它。这里 length-- 后原最后一位虽然还在内存里但已不属于表的一部分。三个关键细节第一*e 必须在搬移元素之前赋值否则元素一搬就拿不到原值第二删除后如果要再插入length-- 其实给表腾出了一个空位不管是满表还是接近满表这个操作能让插入重新合法第三平台如果提供的是 void 原型*e 可以忽略但边界判断不能省。4. 后三关取值、查找、遍历与综合——每一步都是对长度的判断4.1 第4关取值与查找取值是 O(1)查找是 O(n)返回语义别混取值 GetElem 是按位序拿元素复杂度 O(1)查找 LocateElem 是按值找位序复杂度 O(n)。这两个最容易混的是返回值含义取值返回元素本身查找返回元素所在位序找不到返回 0。// 按位取值把第 i 个元素通过 *e 带出 int GetElem(SqList L, int i, int *e) { if (i 1 || i L.length) return 0; *e L.data[i - 1]; return 1; } // 按值查找返回第一个值为 e 的元素的位序找不到返回 0 int LocateElem(SqList L, int e) { int i; for (i 0; i L.length; i) { if (L.data[i] e) return i 1; } return 0; }这里把 L 按值传递是因为这两个操作只读不改传值更安全也避免写出“有副作用的查找”。平台如果给的是指针型 L比如 LocateElem(SqList *L, int e)内部写法改成 L- 即可逻辑完全一样。LocateElem 返回 0 和位序 1 有歧义吗有但教材约定位序从 1 开始0 专门表示“没找到”所以可接受。真正要留意的是如果 ElemType 是结构体类型不能用 比较需要逐字段比较。六关的前几关基本都是 int直接 没有问题但一旦后面换了类型别硬写。参数说明GetElem 的 *e 是输出参数调用方要事先准备一个 int 变量接收LocateElem 的 e 是输入参数它和表中的元素类型应保持一致。若平台要求返回的是 bool 而不是位序就把 return i 1 改成 return 1把最后的 return 0 保持。4.2 第5关遍历输出与清空销毁格式对齐和 length 边界同样致命遍历输出关卡平台判题直接比对输出字符串所以格式比逻辑更伤人两个数字之间有没有空格、末尾换不换行都会判错。最稳的输出写法是除最后一个元素外补空格最后统一换行。// 遍历输出所有元素空格分隔末尾换行 void ListTraverse(SqList L) { int i; for (i 0; i L.length; i) { if (i 0) printf( ); // 第一个元素前无空格 printf(%d, L.data[i]); } printf(\n); // 无论表是否为空都换行 }注意空表时这个函数只输出一个换行。有些题目期望空表输出空行有些期望什么都不输出区别只能从样例输出里判断。样例有空行就保留换行没有就把 printf(\n) 删掉。不要把换行写在循环内部否则每输出一个数字就换一次行直接格式错。清空和销毁是两个操作ClearList 只把 length 置 0物理内存还在后续插入会覆盖旧数据DestroyList 要 free 掉 data并把 data 置 NULL防止悬垂指针。// 清空逻辑上变为空表内存不释放 void ClearList(SqList *L) { L-length 0; } // 销毁释放内存并置空指针 void DestroyList(SqList *L) { if (L-data ! NULL) { free(L-data); L-data NULL; // 置空防止悬垂指针 } L-length 0; }清空和销毁在短小的 main 里差别不明显但放到长生命周期程序里就非常关键不释放内存会泄漏释放后不置 NULL 会悬垂后续万一再 free 一次就是 double free。关卡里考的就是你分不分得清这两步。4.3 第6关综合操作有序表合并与原地逆置先想清楚谁覆盖谁第6关通常是综合题最常出现的是有序表合并和原地逆置。合并函数要格外注意Lc 必须提前初始化并分配足够容量至少能容纳 La.length Lb.length 个元素合并过程用双指针比较谁小谁先进 Lc。// 合并两个非递减有序表 La、Lb 到 Lc结果仍非递减 void MergeList(SqList La, SqList Lb, SqList *Lc) { int i 0, j 0, k 0; // 双指针比较谁小谁先放入 while (i La.length j Lb.length) { if (La.data[i] Lb.data[j]) { Lc-data[k] La.data[i]; } else { Lc-data[k] Lb.data[j]; } } // 剩余部分直接搬入 while (i La.length) { Lc-data[k] La.data[i]; } while (j Lb.length) { Lc-data[k] Lb.data[j]; } Lc-length k; // 统一维护长度 }合并最容易翻车的点忘记给 Lc 初始化或者 Lc 和 La 指向同一块内存。如果 Lc 和 La 共用内存还没开始比较就把源数据覆盖了。稳妥做法是调用前单独 InitList(Lc, La.length Lb.length)。逆置用双指针头尾交换比新建数组省一半空间。循环条件是 i j写成 会把中间元素换两次等于没换。// 原地逆置头尾交换直到指针相遇 void ReverseList(SqList *L) { int i 0, j L-length - 1; while (i j) { int tmp L-data[i]; L-data[i] L-data[j]; L-data[j] tmp; i; j--; } }综合题还有一个暗坑题目可能要求“逆置后遍历输出”你逆置对了但遍历函数沿用了旧输出格式或者空表时多输出了一行导致全错。所以第6关做完要把第5关的输出函数一起检查不能只盯着算法本身。5. 避坑顺序表六关里的五个高频翻车点现象、原因与解决5.1 段错误但本地明明能跑malloc 没判空或访问越界现象提交后在平台上报段错误同一份代码在本地编译运行一切正常让人怀疑是平台“玄学”。原因本地测试数据小堆内存充裕malloc 基本不会失败平台用超大容量或极端用例时malloc 返回 NULL代码没判空直接访问 L-data[0]立刻崩溃。还有一个隐蔽版本插入循环边界写错访问 data[length] 越界本地数组越界不一定会崩但平台内存检测会拦截。解决初始化函数里 malloc 后立即判空插入、删除里每次访问下标前先做位置合法性检查。如果怀疑越界在本地把容量调小到 5然后连续插入 20 个元素崩溃就能复现。这是用本地手段模拟平台环境差异的实用办法。5.2 插入后表变成一整排相同的数for 搬移方向写反现象插入 10 再插入 20输出正常 10 20连续插入几次后出现 10 10 10 这样的数据。原因插入的 for 循环写成从前往后搬data[j1] data[j] 把前面的值不断往后复制后面的值全被覆盖。解决把 for (j 0; j L-length; j) 改为 for (j L-length - 1; j i - 1; j--)。判断口诀插入从尾巴往前搬删除从开头往后搬。每次写完这个循环先念一遍方向再继续写下一行。5.3 删除后最后一个元素“删不掉”length 减了但调用方没拿到现象表有 5 个元素删除第 3 个后遍历仍有 5 个数输出最后一位还是原第 5 个元素。原因遍历用的是旧长度或者函数形参写成 SqList L 而不是 SqList *L函数内部的 L.length-- 只改了副本main 里的 L.length 根本没变。解决修改表内容的函数一律传 SqList *L只读函数传 SqList L 或指针都行。提交前检查每个会修改 length 的函数签名确保形参带指针同时在 main 里 printf(%d, L.length) 确认变成 4。5.4 输出多一个空格或少一个换行平台判“格式错误”现象逻辑完全正确插入、删除、查找都对但提交后还是错本地看输出和样例一样。原因平台比较输出整体字符串。多一个尾随空格、少一个换行、空表时多输出一行都会判不同。本地终端有时会自动吞掉行尾空格所以肉眼看不出来。解决遍历输出用 printf(%d, data[i]) 与 printf( ) 分离控制最后统一 printf(\n)。提交前把样例输出复制到文本对比工具里逐字符比对尤其注意最后一行之后有没有多余换行。如果本地看没问题平台依旧报错把输出语句改到与样例完全一致不要“好心”加提示信息。5.5 函数交了但平台说“该函数未定义”签名先对齐再写实现现象本地编译通过提交后报编译错误提示某个函数未定义或参数不匹配。原因平台的函数原型和你实现的接口对不上。常见差异平台要求 int InitList(SqList *L, int capacity)你写成了 void InitList(SqList *L)平台用 ElemType 作为元素类型你写死了 int平台传表用 L你按 *L 写指针类型不匹配。解决先把题面给的函数原型原样贴进本地文件再填函数体不要自己造一套签名。元素类型优先用题面里的 ElemType并补上 typedef。我常在“按记忆写旧签名”上翻车后来学乖了第一件事永远是把题面里的结构体和函数声明复制下来再开始写实现。6. 验证与进阶写完六关后我总会多做这三步第一给自己建一个“最小边界测试集”。我不只测正常插入还会测空表上删除第 1 个元素、满表时再插入、在 length1 位置插入、删除最后一个元素、表只有一个元素时逆置。这些用例覆盖了平台最喜欢测的边界本地全部跑通后提交基本一次过// 边界自测空表删除、满表插入、单元素逆置 SqList L; int e; InitList(L, 5); printf(del%d\n, ListDelete(L, 1, e)); // 期望 0 for (int i 0; i 5; i) ListInsert(L, 1, i); printf(full%d\n, ListInsert(L, 6, 99)); // 期望 0 ReverseList(L); ListTraverse(L); // 期望 4 3 2 1 0第二把每一步操作都换算成复杂度再决定写法。按位取值 O(1)插入删除 O(n)按值查找 O(n)。这个直觉比背复杂度表有用面试里被问“为什么顺序表插入最坏要搬 n 个元素”能立刻答出因为每搬一次是一个单位时间位置越靠前搬得越多也能马上想到在频繁头部插入删除且不要求随机访问时应该考虑链表。第三把六关的代码保留成一套标准模板。我不按平台一关一关覆盖式粘贴而是把初始化、插入、删除、查找、遍历五个函数单独存成一个文件加上边界测试 main。以后再遇到任何顺序表题直接套模板改返回类型和输出逻辑就行。这个习惯在后续课设和机考时帮我省了大量时间。顺序表六关真正留下的不是那几十行代码而是对“连续内存代价”的判断哪里该检查边界、哪里该搬元素、哪里该改长度。如果有一关卡很久不用怀疑自己笨回头查一下传参是不是少了指针多半就是答案。希望帮到你。本文还有配套的精品资源点击获取