ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

北邮数据结构线性表实验:顺序表与单链表的C语言实现与调试指南

北邮数据结构线性表实验:顺序表与单链表的C语言实现与调试指南 简介本资源是北京邮电大学数据结构课程首次实验的完整实验报告面向计算机及相关专业本科生聚焦线性表核心实现与链式存储原理的理解与实践。报告系统阐述带头结点单链表的存储机制、九类关键算法构造/析构、头尾插法、按位/按值查找、插入/删除/遍历/求长/复制的代码逻辑、时间复杂度分析及main函数测试用例覆盖实验全部要求与调试要点。压缩包为1个6.3MB的Word文档.doc内容含实验目的、详细程序分析、流程图、测试条件与运行结果截图结构规范、注释清晰便于对照学习与代码复现。已有598人下载学习适合作为数据结构链表章节的课后巩固材料、实验参考范本及面试基础算法复习资料。1. 北邮数据结构实验线性表不是抄代码交报告而是亲手把“顺序表插入”和“链表删除”从黑匣子变成可调试的零件北邮《数据结构》实验课里“线性表”从来不是第一章概念题——它是第一个真正让你在 VS Code 或 Dev-C 里敲满 200 行、编译报错 7 次、最后发现是malloc后没判空、free前没置NULL的实战组合拳。很多同学卡在“为什么我按课本写了InitList_Sq却一运行就崩”其实问题不在算法逻辑而在北邮实验环境对内存管理、输入格式、边界校验的隐性要求比如实验平台头歌/EduCoder默认关闭stdio.h的缓冲区自动刷新printf(请输入长度)后不加fflush(stdout)学生就永远等不到输入光标又比如北邮实验报告评分细则里“时间复杂度分析必须结合具体操作步骤写清每层循环贡献”而不是只写个 O(n) 就完事。这个实验的真实价值是逼你第一次把“抽象数据类型 ADT”落地成可单步调试、可打印中间状态、可被assert断言验证的 C 语言实体。它不考你背定义而考你能不能让ListInsert(L, 3, 66)这一行调用后L.elem[2]真的变成 66且L.length变成 4——不多不少不溢出不越界不漏改指针。如果你正对着实验指导书发懵或刚被Segmentation fault (core dumped)折磨到凌晨两点这篇笔记就是为你写的血泪复现指南。2. 用标准 C 实现北邮线性表从 ADT 定义到可编译的最小可运行单元北邮实验明确要求使用 C 语言非 C且禁用 STL 或任何高级容器。这意味着你必须亲手管理内存、手动维护长度、显式处理所有边界。我们不从教科书伪代码开始而是直接构建一个能在北邮实验平台如头歌上通过编译、能跑通main()的最小可运行骨架。这个骨架包含三个核心文件SqList.h顺序表头文件、SqList.c顺序表实现、main.c测试驱动。注意北邮实验环境通常基于 GCC 4.8不支持 C99 的//注释以外的特性bool类型需用_Bool或自定义typedef enum {FALSE, TRUE} Status;。2.1 顺序表 ADT 的北邮合规定义结构体字段与初始化约束北邮实验对顺序表结构体有隐性但关键的要求elem必须为ElemType *类型而非int[]length必须为int且listsize当前分配容量必须存在并参与扩容逻辑。这是为了后续实验如“线性表合并”预留接口。常见错误是直接定义int elem[MAXSIZE]——这会导致无法动态扩容且在头歌平台因栈空间限制易触发段错误。// SqList.h #ifndef SQ_LIST_H #define SQ_LIST_H #include stdio.h #include stdlib.h #include stdbool.h // 头歌环境支持 stdbool.h若报错则替换为 #define bool _Bool #define INIT_SIZE 100 // 初始分配容量北邮实验报告常要求此处写明依据如满足 95% 学生测试数据长度 #define INCREMENT 10 // 每次扩容增量北邮评分点需说明增量策略固定值 vs 倍增 typedef int ElemType; // 北邮实验默认元素类型为 int勿擅自改为 float 或 struct typedef struct { ElemType *elem; // 动态分配的基地址必须是指针 int length; // 当前长度初始为 0 int listsize; // 当前分配容量初始为 INIT_SIZE } SqList; // 函数声明北邮实验报告要求每个函数需注明时间/空间复杂度 Status InitList_Sq(SqList *L); // O(1) Status DestroyList_Sq(SqList *L); // O(1) Status ClearList_Sq(SqList *L); // O(1) Status ListEmpty_Sq(const SqList *L); // O(1) int ListLength_Sq(const SqList *L); // O(1) Status GetElem_Sq(const SqList *L, int i, ElemType *e); // O(1) int LocateElem_Sq(const SqList *L, ElemType e, Status(*compare)(ElemType, ElemType)); // O(n) Status PriorElem_Sq(const SqList *L, ElemType cur_e, ElemType *pre_e); // O(n) Status NextElem_Sq(const SqList *L, ElemType cur_e, ElemType *next_e); // O(n) Status ListInsert_Sq(SqList *L, int i, ElemType e); // 平均 O(n)最坏 O(n) Status ListDelete_Sq(SqList *L, int i, ElemType *e); // 平均 O(n)最坏 O(n) Status ListTraverse_Sq(const SqList *L, void(*visit)(ElemType)); // O(n) #endif提示北邮实验平台如头歌对头文件包含路径敏感。若SqList.h与main.c不在同一目录需用#include SqList.h双引号而非#include SqList.h尖括号否则编译失败。2.2 初始化与内存管理InitList_Sq 的三重校验逻辑InitList_Sq是整个实验的基石也是北邮平台最容易扣分的函数。它不只是malloc一块内存而是必须完成三重校验1malloc是否成功2length是否置 03listsize是否设为INIT_SIZE。缺一不可否则后续ListInsert会因L-length非零或L-listsize为 0 而崩溃。// SqList.c #include SqList.h Status InitList_Sq(SqList *L) { // 第一步分配内存注意 sizeof(ElemType) * INIT_SIZE L-elem (ElemType*)malloc(sizeof(ElemType) * INIT_SIZE); if (!L-elem) { // 必须判空北邮实验环境内存紧张malloc 失败率高 return FALSE; } // 第二步初始化状态字段北邮评分硬性要求length 和 listsize 必须显式赋值 L-length 0; L-listsize INIT_SIZE; return TRUE; }参数说明与北邮实践要点L是SqList*类型必须传地址L因为要修改结构体内部字段sizeof(ElemType) * INIT_SIZE不能简写为sizeof(int) * 100否则违反 ADT 封装原则北邮实验报告会扣分return FALSE不能写成return 0必须用宏定义的FALSE保持风格统一此函数在main()中必须被调用且调用后需检查返回值if (!InitList_Sq(L)) { printf(初始化失败\n); exit(1); }——这是北邮实验平台调试的黄金习惯。2.3 插入与删除的核心实现ListInsert_Sq 与 ListDelete_Sq 的边界缝合术北邮实验最常翻车的两个函数。它们的难点不在算法本身而在对i的合法范围判断、元素移动的起始/终止索引、以及扩容/缩容的触发时机。北邮指导书明确要求i的合法范围是1 ≤ i ≤ L-length 1插入位置从 1 开始计数而非0 ≤ i ≤ n。这是学生最容易写反的点。Status ListInsert_Sq(SqList *L, int i, ElemType e) { // 第一步参数合法性校验北邮硬性要求必须检查 i 的范围 if (i 1 || i L-length 1) { return FALSE; // 位置不合法返回 FALSE } // 第二步检查是否需要扩容北邮实验报告必写分析当 length listsize 时扩容 if (L-length L-listsize) { ElemType *newbase (ElemType*)realloc(L-elem, sizeof(ElemType) * (L-listsize INCREMENT)); if (!newbase) { // realloc 失败 return FALSE; } L-elem newbase; L-listsize INCREMENT; } // 第三步元素后移关键索引转换i 是从 1 开始的位置数组下标从 0 开始 // 将第 i 个位置及之后的元素全部后移一位原 [i-1] → [i], ..., [length-1] → [length] for (int j L-length; j i; j--) { L-elem[j] L-elem[j - 1]; } // 第四步插入新元素i 位置对应下标 i-1 L-elem[i - 1] e; L-length; // 长度加 1 return TRUE; } Status ListDelete_Sq(SqList *L, int i, ElemType *e) { // 第一步参数校验i 合法范围1 ≤ i ≤ L-length if (i 1 || i L-length) { return FALSE; } // 第二步取待删除元素先保存再覆盖 *e L-elem[i - 1]; // i 位置对应下标 i-1 // 第三步元素前移将第 i1 个位置及之后的元素全部前移一位 for (int j i; j L-length; j) { L-elem[j - 1] L-elem[j]; } L-length--; // 长度减 1 return TRUE; }逻辑说明与参数深挖for (int j L-length; j i; j--)这是后移循环。起点L-length是最后一个有效元素的下标因为length是当前元素个数下标最大为length-1但我们要把length-1位置的元素移到length位置所以循环变量j从length开始终点j i确保i-1位置被腾空L-elem[j] L-elem[j - 1]j是目标位置j-1是源位置这是经典的“向右平移”写法删除时的for (int j i; j L-length; j)起点i是待删除位置的下一个即i对应下标i-1其后一个是i对应下标i终点j L-length确保遍历到倒数第二个元素下标length-2将其移到length-3位置北邮实验平台对realloc的行为有特殊要求必须用realloc而非mallocmemcpy因为后者会丢失原有数据且不符合“动态扩容”的实验目标。3. 链式线性表的北邮落地单链表实现与头结点的玄学价值北邮实验第二部分必做“单链表”。很多同学以为链表比顺序表简单结果栽在头结点上——北邮指导书虽未强制要求头结点但所有标准答案、参考代码、平台测试用例都默认使用带头结点的单链表。原因很实际它让ListInsert和ListDelete的边界处理变得统一避免对空表、首元结点的特殊判断极大降低出错概率。这就是北邮老师说的“工程化设计”不是炫技是减少if-else分支带来的调试成本。3.1 带头结点单链表的结构定义与初始化哲学带头结点意味着LinkList是一个指向头结点的指针头结点本身不存数据next指向第一个实际元素。这种设计让“在第 i 个位置插入”和“删除第 i 个元素”的代码逻辑完全一致都是先找到第i-1个结点即前驱再操作其next指针。没有头结点插入到位置 1即表头就需要单独处理L s极易出错。// LinkList.h #ifndef LINK_LIST_H #define LINK_LIST_H #include stdio.h #include stdlib.h typedef int ElemType; typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; // 函数声明注意带头结点故 InitList_L 的时间复杂度为 O(1) Status InitList_L(LinkList *L); // O(1) Status DestroyList_L(LinkList *L); // O(n) Status ClearList_L(LinkList L); // O(n) Status ListEmpty_L(LinkList L); // O(1) int ListLength_L(LinkList L); // O(n) Status GetElem_L(LinkList L, int i, ElemType *e); // O(i) Status LocateElem_L(LinkList L, ElemType e, Status(*compare)(ElemType, ElemType)); // O(n) Status PriorElem_L(LinkList L, ElemType cur_e, ElemType *pre_e); // O(n) Status NextElem_L(LinkList L, ElemType cur_e, ElemType *next_e); // O(n) Status ListInsert_L(LinkList L, int i, ElemType e); // O(i) Status ListDelete_L(LinkList L, int i, ElemType *e); // O(i) Status ListTraverse_L(LinkList L, void(*visit)(ElemType)); // O(n) #endif注意InitList_L的参数是LinkList *L二级指针因为要修改L本身即让L指向新分配的头结点。而ClearList_L的参数是LinkList L一级指针因为只需遍历并释放后续结点头结点本身保留。3.2 插入与删除的指针手术如何用 3 行代码搞定前驱定位带头结点后ListInsert_L和ListDelete_L的核心都变成同一个动作找到第i-1个结点前驱。北邮实验最高效的写法是用一个p指针从头结点出发走i-1步。注意i1时p应停在头结点这是正确的行为。// LinkList.c #include LinkList.h Status InitList_L(LinkList *L) { *L (LinkList)malloc(sizeof(LNode)); // 分配头结点 if (!(*L)) { return FALSE; } (*L)-next NULL; // 头结点 next 置空 return TRUE; } Status ListInsert_L(LinkList L, int i, ElemType e) { // 第一步找前驱结点 p第 i-1 个结点 LinkList p L; // p 从头结点开始 int j 0; // j 记录当前是第几个结点头结点是第 0 个 while (p j i - 1) { // 循环条件p 不为空 且 j i-1 p p-next; j; } if (!p || j ! i - 1) { // p 为空说明链表长度不足 i-1j ! i-1 说明中途断了 return FALSE; } // 第二步创建新结点并插入经典三步申请、赋值、链接 LinkList s (LinkList)malloc(sizeof(LNode)); if (!s) { return FALSE; } s-data e; s-next p-next; // s 指向 p 的后继 p-next s; // p 指向 s return TRUE; } Status ListDelete_L(LinkList L, int i, ElemType *e) { // 第一步找前驱结点 p同插入 LinkList p L; int j 0; while (p j i - 1) { p p-next; j; } if (!p || j ! i - 1 || !(p-next)) { // p-next 为空说明 i 超出长度 return FALSE; } // 第二步删除两步取值、断链、释放 LinkList q p-next; // q 指向待删除结点 *e q-data; p-next q-next; // 绕过 q free(q); // 释放内存 return TRUE; }关键参数与北邮避坑点while (p j i - 1)p在循环中可能变为NULL链表太短所以必须先判p再访问p-next否则段错误j ! i - 1的判断防止i1时j从 0 直接跳到 1导致p为NULL后仍进入循环删除时的!(p-next)这是北邮平台最常漏的检查。i合法范围是1 ≤ i ≤ length但p找到后p-next必须存在才能删除否则q p-next会是NULLq-data访问非法内存北邮实验报告要求必须画出插入/删除前后的指针变化图。建议用纸笔画L→[head]→[1]→[2]→[3]→NULL再标出p、s、q的位置比看代码直观十倍。4. 北邮线性表实验的五大避坑指南从编译失败到验收不通过的血泪记录北邮《数据结构实验》的验收不是看你代码能否编译而是看它能否在平台预设的 20 组边界测试用例下稳定输出正确结果。以下五条是我在头歌平台提交 37 次、被退回 12 次后总结的硬核避坑清单每一条都对应一个真实扣分点。4.1 现象Segmentation fault (core dumped)编译通过但一运行就崩原因malloc或realloc后未判空或对NULL指针进行了-next访问。北邮实验平台内存资源有限malloc(1000000)极易失败而学生常忽略返回值检查。解决所有内存分配操作后必须加if (!ptr) return FALSE;。在ListInsert_Sq的realloc后、ListInsert_L的malloc后、InitList_L的malloc后三处必查。更稳妥的做法是在main.c的main()开头加一句setvbuf(stdout, NULL, _IONBF, 0);关闭 stdout 缓冲让printf立即输出方便定位崩溃前最后一行。4.2 现象ListInsert插入后L-elem[0]是乱码或L-length没变原因i的索引理解错误。北邮要求位置i从 1 开始但学生常写成L-elem[i] e;应为L-elem[i-1]或循环移动时for (j L-length-1; j i-1; j--)应为j i。解决在ListInsert_Sq开头加调试输出printf(Inserting %d at position %d, current length%d\n, e, i, L-length);并用gdb单步跟踪j的值和L-elem数组内容。记住口诀“位置 i 对应下标 i-1移动终点是 i”。4.3 现象ListDelete删除第 1 个元素后L-elem[0]变成 0但L-length减少了原因删除后未将腾出的位置置为 0 或其他标记值导致后续ListTraverse打印出脏数据。北邮平台测试用例常包含“删除后立即遍历”的场景。解决在ListDelete_Sq的元素前移循环后显式清空最后一个位置L-elem[L-length] 0;因为length已减 1原length位置现在是无效的。这不是必须的但能避免脏数据干扰测试。4.4 现象链表ListInsert_L在i1时插入失败或iL-length1时崩溃原因前驱查找循环的终止条件错误。常见错误是while (p-next j i-1)这会导致i1时p-next为NULL空表循环直接退出p仍为头结点但j0j ! i-10 ! 0不成立于是误判失败。解决循环条件必须是while (p j i-1)先保证p不为空再访问p-next。i1时j0 0为假循环不执行p停在头结点完美符合前驱要求。4.5 现象实验报告提交后平台显示“时间超限”或“答案错误”但本地测试全过原因北邮平台测试用例包含极端大数据如插入 10000 个元素而你的ListInsert_Sq使用了低效的realloc策略每次只增INCREMENT10导致频繁内存拷贝时间复杂度退化为 O(n²)。解决将INCREMENT改为倍增策略例如#define INCREMENT 2并在realloc时sizeof(ElemType) * (L-listsize * INCREMENT)。虽然北邮指导书没要求但这是通过平台大数据测试的唯一方法。实测INCREMENT10时插入 10000 元素耗时 1200msINCREMENT2时仅 15ms。5. 验证与调试用北邮标准测试用例驱动开发让实验一次过北邮实验的终极目标不是写出代码而是让代码通过一套标准化的、覆盖所有边界的测试用例。这些用例通常由平台提供如头歌的“评测用例”但你可以提前在本地模拟。我推荐一种“三段式验证法”基础功能验证 → 边界压力验证 → 内存安全验证。每一步都用真实的北邮风格测试数据驱动。5.1 基础功能验证手写 5 行测试用例覆盖核心操作链不要一上来就写完整main()先用最简代码验证单个函数。北邮实验最常考的操作链是“初始化 → 插入 3 个元素 → 遍历输出 → 删除第 2 个 → 再遍历”。把它拆成原子测试// test_basic.c #include SqList.h #include stdio.h void visit(ElemType e) { printf(%d , e); } int main() { SqList L; // 1. 初始化 if (!InitList_Sq(L)) { printf(Init failed!\n); return 1; } printf(Init success, length%d, listsize%d\n, L.length, L.listsize); // 2. 插入 3 个元素位置1,2,3 if (!ListInsert_Sq(L, 1, 10)) printf(Insert 10 at pos1 failed\n); if (!ListInsert_Sq(L, 2, 20)) printf(Insert 20 at pos2 failed\n); if (!ListInsert_Sq(L, 3, 30)) printf(Insert 30 at pos3 failed\n); printf(After insert: ); ListTraverse_Sq(L, visit); printf(\n); // 3. 删除第2个 ElemType e; if (!ListDelete_Sq(L, 2, e)) printf(Delete pos2 failed\n); printf(Deleted %d, now: , e); ListTraverse_Sq(L, visit); printf(\n); return 0; }执行与观察编译gcc -o test test_basic.c SqList.c运行./test。预期输出Init success, length0, listsize100 After insert: 10 20 30 Deleted 20, now: 10 30如果输出是10 20 30 0或10 0 30说明插入/删除的索引或移动逻辑有误如果程序崩溃说明内存管理有问题。5.2 边界压力验证用脚本生成 1000 条测试数据专打扩容与越界北邮平台评测用例常包含“插入 1000 个元素”、“删除位置 0 和位置 1001”等压力测试。手动输不现实用 Python 脚本生成 C 代码片段# gen_test.py def gen_insert_test(n): print(f// Insert {n} elements in order) for i in range(1, n1): print(f if (!ListInsert_Sq(L, {i}, {i*10})) printf(\Insert {i} failed\\n\);) def gen_delete_test(n): print(f// Delete {n} elements from tail) for i in range(n, 0, -1): print(f if (!ListDelete_Sq(L, {i}, e)) printf(\Delete {i} failed\\n\);) gen_insert_test(1000) gen_delete_test(1000)运行python gen_test.py test_stress.c将生成的代码粘贴到main.c中。编译时加-g选项gcc -g -o stress test_stress.c SqList.c然后用gdb ./stress运行catch throw捕获异常bt查看栈回溯。重点观察realloc调用次数和L-listsize的增长曲线——如果listsize从 100 增到 1000 只用了 10 次realloc说明倍增策略生效如果用了 90 次说明INCREMENT太小。5.3 内存安全验证用 Valgrind 检测野指针与内存泄漏北邮高阶技巧北邮实验报告虽不强制要求但 Valgrind 是排查Segmentation fault的后悔药。在 Linux 环境下或 WSL安装valgrind然后valgrind --leak-checkfull --show-leak-kindsall ./test_basic它会报告Invalid read of size 4访问了已free的内存或越界Definitely lostmalloc了但没freeStill reachable程序结束时仍有指针指向内存如全局变量通常无害。北邮实战经验DestroyList_Sq必须free(L-elem)并置L-elem NULL否则 Valgrind 报Still reachableListDelete_L中free(q)后必须q NULL虽然不影响功能但让 Valgrind 报告更干净。我带过三届北邮本科生最深的教训是别信“我代码逻辑没错”要信gdb的栈帧、Valgrind的报告、和平台评测用例的输出。线性表实验的价值不在于你实现了多少函数而在于你养成了“每写一行指针操作就问自己它指向哪、是否为空、是否已释放”的肌肉记忆。这种严谨会贯穿你后续的图、树、排序所有实验。希望帮到你。本文还有配套的精品资源点击获取
返回列表