资讯动态

东北大学数据结构实验:顺序表动态扩容、带尾指针单链表与约瑟夫环实战

发布时间:2026/10/14 10:55:13 来源:尧图企业网站定制
简介本资源是东北大学《数据结构》课程的完整实验报告面向计算机专业本科生及数据结构初学者聚焦线性表核心应用——约瑟夫环问题的链式实现与算法分析。报告涵盖实验目的、循环链表设计含data编号与password密码双字段、CreatList_CL建表、Display显示、Delete_L出列等关键函数代码及详细时空复杂度分析如Delete_L为O(mn)并记录了Windows 7 Visual Studio 2017环境下的典型调试问题与修正过程如头结点处理、指针重定向等极具实操参考价值。资源为单文件docx文档大小502KB内容结构完整含实验编号、指导教师、班级学号、步骤说明、代码片段、测试结果与反思总结。目前已有291人学习下载适合用于课程作业参考、算法实现复现、链表操作强化训练及复杂度分析入门实践。1. 东北大学数据结构实验报告.docx不是模板套用而是把顺序表、单链表、约瑟夫环三个核心实验真正跑通、调对、写进报告的实操路径你手头有一份名为“东北大学数据结构实验报告.docx”的文件但打开后发现——代码空着、截图没贴、分析段落全是占位符甚至测试数据都写着“此处填入运行结果”。这不是文档缺陷而是典型教学场景下的真实起点老师发的是框架学生要填的是可验证、可复现、能体现思考过程的完整实验闭环。本报告绝非Word排版作业它承载着三个硬核落地任务用C语言或C实现顺序表的动态扩容与定位插入、构建带尾指针的单链表并完成指定位置建表与遍历、最后用循环单链表模拟约瑟夫环并输出淘汰序列。网上搜“东北大学mirror”或“王道408数据结构代码”常看到零散片段而严蔚敏《数据结构C语言版》第2章的习题要求恰恰就卡在这三类结构的内存布局差异、边界条件处理、以及销毁逻辑是否彻底上。本文不讲概念复述只带你从main()函数开始一行行敲出能编译、能调试、能截图、能写进报告“实验结果与分析”栏的真代码——包括为什么InitList_Sq必须传二级指针、为什么ListInsert_L在i1时要改头结点、为什么约瑟夫环里p-next p才是最后一人。适合正在赶DDL的大二学生也适合想用真实教学案例练手的助教。2. 顺序表从静态数组到动态扩容用realloc实现真正的“可变长”存储顺序表是数据结构实验的首道关卡但多数人卡在“看似能跑实则内存越界”上。东北大学实验指导书明确要求顺序表需支持动态增长初始容量为10当插入导致溢出时自动扩容至原容量1.5倍向上取整。这直接否定了#define MAXSIZE 100的静态写法——它无法体现“动态性”这一核心能力更会在后续链表对比实验中暴露设计缺陷。2.1 结构体定义与初始化为什么InitList_Sq必须传SqList**严蔚敏教材中InitList_Sq原型为Status InitList_Sq(SqList *L)但实际编码时若按此写法在main()中传入L后L-elem (ElemType*)malloc(...)虽能分配内存却无法让L本身指向新地址因*L是值拷贝。真实踩坑点在于当顺序表首次扩容时realloc可能返回新地址原指针失效。若未用二级指针传递主函数中的L仍指向旧内存后续所有操作全错。// 正确写法使用二级指针确保L本身被更新 Status InitList_Sq(SqList **L) { *L (SqList*)malloc(sizeof(SqList)); if (!(*L)) return OVERFLOW; (*L)-elem (ElemType*)malloc(10 * sizeof(ElemType)); if (!(*L)-elem) { free(*L); return OVERFLOW; } (*L)-length 0; (*L)-listsize 10; // 初始容量 return OK; }提示SqList定义中ElemType* elem是关键——它声明了“元素存储区由malloc动态申请”而非栈上数组。listsize字段记录当前分配总容量非当前长度这是判断是否需扩容的唯一依据。2.2 插入操作定位插入的边界校验与内存重分配逻辑实验报告要求“在第i个位置插入e”即插入后e成为新表的第i个元素1≤i≤length1。常见错误是忽略i1和ilength1的特殊处理或扩容后未更新listsize。以下代码严格遵循教材算法且每步附校验Status ListInsert_Sq(SqList **L, int i, ElemType e) { // 1. 位置合法性检查i必须在[1, length1]闭区间 if (i 1 || i (*L)-length 1) return ERROR; // 2. 容量检查若已满触发扩容 if ((*L)-length (*L)-listsize) { int newsize (int)((*L)-listsize * 1.5); // 向上取整10→15, 15→23... ElemType* newbase (ElemType*)realloc((*L)-elem, newsize * sizeof(ElemType)); if (!newbase) return OVERFLOW; (*L)-elem newbase; (*L)-listsize newsize; } // 3. 元素后移从最后一个元素开始逐个向后挪一位 // 注意移动范围是 [i-1, length-1] 索引0基共 length-i1 个元素 for (int j (*L)-length; j i; j--) { (*L)-elem[j] (*L)-elem[j-1]; } // 4. 插入新元素长度1 (*L)-elem[i-1] e; // i为1基转为0基索引 (*L)-length; return OK; }参数说明i插入位置1基序号i1表示插到表头ilength1表示插到表尾newsize计算采用(int)(old * 1.5)而非old 5因后者在大容量时增长过慢如1000→1005不符合“动态”要求for循环中j从length开始当前末尾索引1终止于i确保elem[i-1]被腾出空间。2.3 实验验证用三组数据触发不同分支截图必须含内存地址变化为证明扩容机制生效需设计三组测试用例初始插入插入10个数1~10此时length10,listsize10尚未扩容首次扩容再插入11触发realloc观察elem地址是否变化GDB中p ((*L)-elem)边界插入在i1插入0在i12插入12验证前后端插入一致性。注意VS Code CodeLLDB调试时watch窗口添加(*L)-elem和(*L)-listsize运行至realloc后对比地址。若地址不变说明realloc未实际移动内存小容量时可能复用原空间此时需插入足够多数据如连续插入20个强制迁移。3. 单链表带尾指针的循环建表与指定位置插入彻底规避头结点玄学单链表实验常陷入“头结点该不该有”的争论但东北大学实验报告明确要求“建立单链表时输入n个元素依次插入到表尾”。这意味着必须维护尾指针rear否则每次插入都要遍历到末尾时间复杂度O(n²)违背链表O(1)尾插的设计初衷。而“在指定位置插入”又要求支持任意位置此时头结点head成为必要——它让i1的插入无需特殊判断。3.1 结构体与建表为什么rear指针比length字段更重要教材中LinkList常定义为typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList;但此定义缺失尾指针。真实实验需扩展为typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; typedef struct { LinkList head; // 头指针指向头结点 LinkList rear; // 尾指针指向最后一个结点 int length; // 当前长度可选但报告要求统计 } LinkListWithRear;建表函数CreateList_L必须同时更新head和rearStatus CreateList_L(LinkListWithRear *L, int n) { L-head (LinkList)malloc(sizeof(LNode)); // 创建头结点 if (!L-head) return OVERFLOW; L-head-next NULL; L-rear L-head; // 初始尾指针指向头结点 L-length 0; printf(请输入%d个整数空格分隔\n, n); for (int i 0; i n; i) { LinkList s (LinkList)malloc(sizeof(LNode)); if (!s) return OVERFLOW; scanf(%d, s-data); s-next NULL; // 尾插直接接在rear后面然后rear后移 L-rear-next s; L-rear s; L-length; } return OK; }关键逻辑L-rear-next s将新结点挂到当前尾部L-rear s立即更新尾指针。此操作时间复杂度O(1)与链表长度无关。3.2 指定位置插入头结点如何消解i1的边界判断无头结点时i1插入需修改head指针代码需分支处理有头结点后所有插入统一为“找到第i-1个结点将其next指向新结点”。ListInsert_L实现如下Status ListInsert_L(LinkListWithRear *L, int i, ElemType e) { if (i 1 || i L-length 1) return ERROR; // 位置检查 // 找到第i-1个结点p起始从head出发头结点索引为0 LinkList p L-head; for (int j 1; j i; j) { // j从1到i-1共i-1步 p p-next; } // 创建新结点并插入 LinkList s (LinkList)malloc(sizeof(LNode)); if (!s) return OVERFLOW; s-data e; s-next p-next; p-next s; // 若插入到表尾需更新rear if (i L-length 1) { L-rear s; } L-length; return OK; }参数说明p初始为L-head头结点循环ji次后p指向第i-1个实际结点0基i L-length 1时新结点成为新尾结点必须同步更新L-rear否则后续尾插会断链此写法彻底消除if(i1)分支代码更健壮。3.3 遍历与打印避免无限循环的两个硬性检查链表遍历最易翻车的是忘记判断p ! NULL或误写p p-next位置。标准打印函数必须包含void PrintList_L(LinkListWithRear *L) { LinkList p L-head-next; // 跳过头结点从第一个实际结点开始 printf(链表内容); while (p ! NULL) { // 关键必须判NULL printf(%d , p-data); p p-next; // 移动必须在打印后 } printf(\n表长%d\n, L-length); }提示若打印结果出现乱码或程序崩溃90%概率是p已为NULL却仍执行p-data。GDB中单步至此p值为0x0即确认空指针解引用。4. 约瑟夫环用循环单链表模拟淘汰过程输出序列必须含编号与淘汰顺序约瑟夫环是链表应用的经典题但学生常犯两类错误一是用数组模拟违背“链表应用”实验目标二是循环链表未真正闭环rear-next未指向head。东北大学实验要求“用单链表实现”隐含必须是循环单链表且最终输出格式为“淘汰顺序5, 3, 1, 2, 4”。4.1 循环单链表构建rear-next head 是生命线在CreateList_L基础上仅需增加一行即可闭环Status CreateCircleList_L(LinkListWithRear *L, int n) { // ... 同前建表代码创建头结点、尾插n个结点... // 关键让尾指针指向头结点形成环 L-rear-next L-head; return OK; }验证方法建表后执行printf(环检测%s\n, L-rear-next L-head ? OK : ERROR);必须输出OK。若漏此步后续p-next遍历时会因NULL终止无法循环。4.2 淘汰算法m为报数上限k为起始位置双指针定位更稳教材算法常从头结点开始报数但实验报告要求“从第k个人开始报数”。若直接让p从head-next出发k1时正确k1时需额外移动。更稳妥做法是用双指针pre指向待删结点前驱p指向待删结点初始时pre从head出发走k-1步p pre-next。void Josephus_Circle(LinkListWithRear *L, int m, int k) { if (L-length 0) return; // 1. 定位起始点pre指向第k-1个结点k1时prehead LinkList pre L-head; for (int i 1; i k; i) { pre pre-next; } printf(淘汰顺序); while (L-length 1) { // 剩余1人时停止 // 2. 报数m-1次pre移动m-1步p指向第m个结点 for (int i 1; i m; i) { pre pre-next; } LinkList p pre-next; // p为待删结点 // 3. 删除p跳过p连接前后 pre-next p-next; // 4. 若p是尾结点需更新rear if (p L-rear) { L-rear pre; } printf(%d, p-data); if (L-length 2) printf(, ); // 末尾不加逗号 free(p); L-length--; } printf(, %d\n, L-head-next-data); // 最后一人 }参数说明m报数上限如m3报1,2,3第3人淘汰k起始位置1基k1从第1人开始k3从第3人开始pre-next p-next是删除核心p-next必不为NULL因是循环链表if (p L-rear)判断必不可少否则rear悬空后续操作崩溃。4.3 实验数据设计用n5,m3,k1验证经典序列输入n5人员编号1~5m3k1理论淘汰序列为3,1,5,2,4。运行代码后控制台必须精确输出此序列。若出现3,1,5,2,4,1或提前终止说明while (L-length 1)条件错误应为1非1free(p)后未L-length--导致循环次数错误pre初始位置计算错误k1时pre应为head而非head-next。血泪经验第一次调试时我在free(p)后忘了L-length--导致循环多执行一次p指向已释放内存GDB显示p-data为随机值。加L-length--后问题消失——链表长度必须与实际结点数严格一致这是所有操作的元数据基础。5. 避坑指南顺序表、链表、约瑟夫环三大模块的5个致命错误与修复方案实验报告写得再漂亮代码一跑就崩所有努力归零。以下是我在批改37份东北大学数据结构实验报告中高频出现的5个致命错误每条均按“现象→原因→解决”给出可立即执行的修复动作。5.1 现象顺序表插入后第i个位置数据错乱相邻元素覆盖原因for循环中元素后移方向错误写成for (int j i; j length; j)正向移动导致elem[i]被elem[i1]覆盖连锁反应。解决严格使用逆向移动——for (int j length; j i; j--)从末尾开始确保每个位置被覆盖前已完成赋值。5.2 现象单链表打印时程序崩溃GDB显示EXC_BAD_ACCESS原因PrintList_L中p p-next写在printf之前当p为最后一个结点时p-next为NULLprintf(%d, p-next-data)试图访问NULL-data。解决调整语句顺序——先printf(%d, p-data)再p p-next且循环条件必须为p ! NULL不可省略。5.3 现象约瑟夫环输出序列长度不足n或重复出现同一编号原因Josephus_Circle中while (L-length 1)内删除结点后未更新L-length导致循环次数少于预期或pre初始定位时for循环次数错误如k1时循环0次但代码写了ik。解决在free(p)后立即执行L-length--pre定位循环条件改为i kk1时不进入循环pre保持为head。5.4 现象顺序表扩容后新插入元素出现在错误位置如插入11时10被覆盖原因realloc后未重新赋值(*L)-elem或listsize未更新导致下次插入仍判断为“已满”而再次realloc内存碎片化。解决realloc后必须(*L)-elem newbase;且(*L)-listsize newsize;二者缺一不可建议在realloc后加printf(扩容%d → %d\n, old_size, newsize)日志验证。5.5 现象链表清空后再次插入报错“Segmentation fault”原因ClearList_L函数仅释放结点但未重置L-head-next NULL和L-rear L-head导致rear指向已释放内存后续尾插时L-rear-next s写入非法地址。解决清空函数必须重置所有指针Status ClearList_L(LinkListWithRear *L) { LinkList p L-head-next, q; while (p ! L-head) { // 循环链表终止于回到head q p; p p-next; free(q); } L-head-next NULL; // 重置头结点 L-rear L-head; // 重置尾指针 L-length 0; return OK; }注意循环链表清空必须判断p ! L-head而非p ! NULL否则永远无法退出循环。6. 报告撰写技巧用GDB截图内存地址淘汰序列三要素让助教一眼认可工作量实验报告的价值不在Word排版多精美而在能否证明你亲手敲过、调过、理解过每一行代码。我带过的12届学生中助教快速给高分的报告都有一个共同特征截图不是黑框命令行而是GDB调试界面且同时展示三要素——变量值、内存地址、执行路径。下面以约瑟夫环为例拆解如何用最小 effort努力产出最大 credibility可信度。6.1 GDB截图必须包含的三个区域打开终端编译时加-g参数gcc -g josephus.c -o josephus。启动GDBgdb ./josephus。设置断点并运行(gdb) break Josephus_Circle (gdb) run # 输入n5, m3, k1 (gdb) step # 进入函数 (gdb) print *L # 显示整个LinkListWithRear结构 (gdb) x/10xw L-head # 查看头结点内存10个字十六进制 (gdb) continue截图需截取以下三块可用CtrlShiftA区域截图左上角GDB命令行显示print *L结果含head、rear、length值中间x/10xw输出清晰显示head-next指向的地址如0x5555555592a0及该地址处的data和next字段右下角程序运行后的淘汰序列输出行淘汰顺序3, 1, 5, 2, 4。提示x/10xw中w表示word4字节x表示hex10表示显示10个单元。若data为intnext为指针此命令恰好显示头结点前4个结点的data和next。6.2 报告“实验结果与分析”栏的黄金写法不要写“程序运行正常”要写具体证据。例如实验结果顺序表插入12个元素后(*L)-listsize由10→15→23(*L)-elem地址由0x5555555592a0变为0x5555555593b0证实realloc成功迁移内存。单链表CreateList_L后L-rear-next L-head返回truePrintList_L输出1 2 3 4 5无乱码。约瑟夫环n5,m3,k1时淘汰序列严格为3,1,5,2,4GDB中p-data值与预期完全一致见图3。分析realloc的地址变更证明动态扩容机制有效rear-next head为真确认循环链表构建正确淘汰序列与数学推导一致表明报数逻辑无偏差。所有操作均通过GDB内存视图验证排除指针悬空风险。6.3 一个被90%学生忽略的加分细节在代码中添加// [报告图X]注释在关键函数开头添加一行注释指向报告中的截图编号// [报告图3] Josephus_Circle函数入口用于GDB调试定位 void Josephus_Circle(LinkListWithRear *L, int m, int k) { // ... 函数体 ... }这样助教对照报告时能瞬间将代码行与截图关联直观感受到你的调试深度。我曾见一份报告因在ListInsert_Sq函数第一行写// [报告图1]并在图1中标出realloc调用行直接获得额外2分——因为助教无需猜你调了哪里证据就在眼前。最后说一句实在话这份报告真正难的不是写代码而是把调试过程变成可展示的证据链。我当年交报告前花3小时录屏GDB操作截12张图只为证明rear-next确实指向head。现在回头看那3小时换来的不仅是分数更是对指针本质的肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑