C语言循环链表实现约瑟夫环:建环、出列删除与避坑
发布时间:2026/10/31 3:08:39 锦皓数字建站

简介面向学习C语言数据结构与循环链表应用的学生及初级开发者这份文档围绕经典约瑟夫生死游戏给出完整代码与设计思路重点解决环形链表建模、按固定报数淘汰以及保留指定人数等问题。包内共1个docx文件约18KB以文档形式整合算法逻辑、结构体与链表类型定义、核心函数说明及可直接参考的C语言实现便于在课程实验、算法练习或链表专题复习中对照理解。已有3145人学习说明该主题具有较稳定的查阅需求。读者可从中梳理元素类型、结点类型与链表类型的组织方式理解创建循环链表、删除指定位置结点、循环淘汰和输出剩余人员信息的实现脉络并借助代码与思路对照形成自己的链表算法笔记。1. 一艘船、一圈人、一个报数间隔约瑟夫环为什么非要循环链表约瑟夫死亡游戏这个题目在数据结构课设里出镜率很高给定总人数 TotalCount、报数间隔 PersonCount、需要剩下的 LeftTotal要求输出每个人被抛下海的先后顺序以及最终留在船上的名单。如果只问最后一个幸存者的编号递推公式几行就能算完但这里要的是全过程编号、姓名、出列次序一个都不能少。数组做这件事会很别扭每删一个人后面所有人往前挪一格挪完还得重新接上环的结构完全体现不出来。通常的做法是尾插法建一条带头结点的单链表再让尾结点的 next 指回第一个数据结点形成单向循环链表之后所有删除都退化成改两个指针的事。这套方案适合正在做课程设计、需要一份能跑起来、也能在答辩时讲清每一步的读者。2. 需求拆解与结构体定型ElemType、LNode、LinkList 三层定义各管什么2.1 把「人」和「结点」拆成两层是这套代码最容易被忽略的设计很多同学拿到题目第一反应是直接开一个结构体里面塞编号、姓名、next 指针代码也能跑。但这个题目明确要求「确定元素类型包括两项位置和姓名需要定义结构体类型的元素类型 ElemType」这句话不是废话它规定了数据结构的分层方式数据元素是「人」结点是「装人的盒子」两者分开定义。分层的好处在实际写删除函数的时候会立刻显现。删除操作希望把被删掉的那个人完整地带回来给调用方看如果只有结点类型调用方拿到的是一个已经 free 掉的指针读它就是未定义行为而有了独立的 ElemType函数签名可以写成void DeleteLinkList(LinkList L, int i, ElemType e)删除时先e q-data把整份数据拷贝出来再 free(q)调用方手里握着的是一份不受影响的副本。这一点在输出「抛入大海人员名单」时是刚需。/* 数据元素类型一个人的全部信息与链表结构无关 */ typedef struct ElemType { int position; /* 人员编号建表时按插入次序从 1 开始赋值 */ char name[8]; /* 人员姓名注意长度上限后面避坑章节会专门说 */ } ElemType; /* 结点类型数据域 指针域指针域指向同类型结点所以要用 struct LNode * */ typedef struct LNode { ElemType data; struct LNode *next; } LNode; /* 链表类型本质就是结点指针单独起个名字是为了函数签名读起来像链表 */ typedef LNode *LinkList;这里有个 C 语言里的小知识点值得在答辩时提一句typedef struct ElemType { ... } ElemType;中 struct 标签名和 typedef 名同名是完全合法的因为标签tag和普通标识符在 C 里属于两个不同的命名空间。同理struct LNode内部的struct LNode *next不能写成LNode *next因为此时 typedef 还没生效编译器还不认识 LNode 这个名字。这两处是初学阶段最常被问、也最容易答不上来的地方。参数怎么定也值得说清楚。position 从 1 开始而不是从 0 开始因为报数是人的直觉「第 1 个人」比「第 0 个人」好讲输出名单时也不用每次加一。代价是后面所有涉及下标的计算都要留意这个偏移尤其是j i - 1这种边界判断差一位就翻车。name 长度定 8 是个折中两字中文姓名在 UTF-8 下占 6 字节加结束符刚好 7能装下三字姓名就不够了这个坑留到第 5 章展开。2.2 LinkList 是结点指针类型不是结点本身typedef LNode *LinkList;这一行决定了后面所有函数签名的形态。它表达的意思是链表类型的变量本质上就是一个指向 LNode 的指针。所以LinkList L;声明出来的 L 是一个指针变量占 4 或 8 个字节不是一整个结点。理解这一点才能理解为什么建表函数要用LinkList L而不是LinkList L。/* 错误写法值传递函数内对 L 的修改出不了函数 */ void CreateLinkList(LinkList L, int n); /* 正确写法一C 引用传递函数内改 L 就是改调用方的 L */ void CreateLinkList(LinkList L, int n); /* 正确写法二纯 C 的二级指针写法效果等价但要处处解引用 */ void CreateLinkList(LinkList *L, int n);原始代码用的是第一种LinkList L。这里的是 C 的引用语法不是取地址符。这意味着这份代码必须用 C 编译器编译文件后缀写成.cpp用 g 编译或者退一步改成二级指针版本。拿 gcc 直接编.c文件报错信息通常是expected ) before token第一次遇到的人往往会盯着括号看半天。这条不是玄学是语法层面的硬约束属血泪经验级别的必踩点之一。两级写法各有取舍。引用写法函数体里干净L-next NULL直接写读起来跟操作普通变量一样二级指针写法要多一层解引用(*L)-next NULL但胜在纯 C 环境下通用答辩老师问「能不能用纯 C 写」时不至于卡住。我一般会在方案里保留引用版本作为主实现另外在注释里附一份二级指针的对照写法答辩时主动提一句反而成了加分项。顺带说一个容易混淆的地方LinkList L和LNode *L在编译器眼里是同一个类型完全可以互换。区别只在可读性LinkList三个字母传达的是「这是一个链表」而LNode *传达的是「这是一个结点指针」。函数返回链表头、参数接收链表头的时候用 LinkList遍历过程中指向某个具体结点的临时变量用 LNode *代码读起来层次会清楚很多。2.3 三个参数构成的接口约定TotalCount、PersonCount、LeftTotal整个程序对外只有三个输入但它们之间的关系必须提前约定好否则后面全是边界问题。TotalCount 是初始人数必须为正PersonCount 是报数间隔语义是「数到第几个人把他扔下去」最小为 1LeftTotal 是轮船安全需要剩余的人数合法范围是 0 到 TotalCount 之间。真正需要想清楚的是三者组合出来的语义。当 LeftTotal 等于 TotalCount 时一轮都不用删程序应该直接输出全部人员当 LeftTotal 等于 0 时所有人都会被扔下去最后一轮删完之后链表是空的L 指向一块已经释放的内存此时再去遍历输出就会崩溃当 PersonCount 等于 1 时每一轮删的都是「当前的第一个人」等价于从头到尾挨个删一遍。这三种情况在原始代码里都没有显式处理答辩时被问到「输入 5 3 5 会怎样」很容易答不上来。通常的做法是在 main 里做一次集中校验把非法输入挡在业务逻辑外面。校验逻辑本身不长但它决定了程序是「能跑通一组数据」还是「能跑通所有数据」这两者在验收时的评价差距很大。校验通过之后再调用 DieLinkList后面所有函数就可以假定参数合法不必层层兜底代码反而更清爽。int main(void) { int TotalCount, PersonCount, LeftTotal; LinkList L NULL; printf(请输入总人数); scanf(%d, TotalCount); printf(请输入抛入大海时要数到的人数); scanf(%d, PersonCount); printf(请输入轮船安全需要剩余的人数); scanf(%d, LeftTotal); /* 集中校验把非法组合挡在业务函数之外 */ if (TotalCount 0 || PersonCount 0 || LeftTotal 0 || LeftTotal TotalCount) { printf(输入不合法请满足 总人数0、报数0、0剩余人数总人数\n); return 1; } DieLinkList(L, TotalCount, PersonCount, LeftTotal); DisplayLinkList(L); return 0; }这里把 main 写成int main(void)而不是原代码的void main()是因为后者在 C 标准里从未被允许只是早期编译器放行。改成标准形式加一句return 0;成本几乎为零却能在答辩时避开「你这个 main 返回值怎么是 void」这种无谓的追问。3. 建环这一步头结点、尾插法与 r-next L-next 的成环时机3.1 先用头结点占位再从尾部一个个挂上去带头结点是教材里的标准起手式它的价值在于把「空表」和「非空表」两种情况的代码统一掉。如果没有头结点插入第一个元素时必须单独判空写一句if (L NULL) { L p; }后面每个函数都要重复这个判断。有了头结点L 永远非空插入时r-next p一视同仁代码短一半。void CreateLinkList(LinkList L, int n) { LinkList r, p; /* r 永远指向当前最后一个结点p 是本次新建的结点 */ int i; /* 第一步造一个头结点占位此时还不是环next 置空 */ L (LinkList)malloc(sizeof(LNode)); if (L NULL) { printf(内存分配失败\n); return; } L-next NULL; r L; /* 空表时尾结点就是头结点自己 */ /* 第二步尾插法逐个挂结点编号按插入次序从 1 开始 */ for (i 1; i n; i) { p (LinkList)malloc(sizeof(LNode)); if (p NULL) { printf(内存分配失败\n); return; } p-next NULL; /* 新结点先自成尾巴避免接到旧数据上 */ p-data.position i; printf(请输入第%d个人员姓名, i); scanf(%7s, p-data.name); /* 限宽 7配合 name[8] 防越界 */ r-next p; /* 挂到当前尾部后面 */ r p; /* 尾指针后移到新结点 */ } /* 第三步首尾相接成环然后拆掉头结点这个脚手架 */ r-next L-next; /* 尾结点的 next 指向第一个数据结点 */ free(L); /* 头结点用完即弃 */ L r-next; /* L 重新指向第一个数据结点成为环的入口 */ }逐行看几个关键点。r这个尾指针是整个建表过程的枢纽它省掉了每次插入都从头遍历找尾巴的 O(n) 开销让建表整体保持 O(n)。p-next NULL这一句看起来可有可无因为紧接着r-next p就会把 p 接上去但它是防御性的写法万一后续代码在成环之前还要操作 p至少 p 不会指向一块随机内存。scanf(%7s, ...)里的 7 是宽度限制最多读 7 个字符加一个结束符正好塞满 8 字节防止输入超长姓名把后面的 position 字段冲掉。第三步的顺序是整个函数里最不能错的地方。必须先把r-next L-next做完让环成型再free(L)最后L r-next。三步次序颠倒任何一处都会出问题先 free 再取L-next读的是已释放内存先改 L 再成环r-next 指向的就是 L 自己形成一个只有尾巴的自环。这个顺序我一般会在黑板上画三格示意图答辩时照着图讲一遍比背代码有效得多。还有一个隐藏的边界n 等于 0 的时候循环一次都不执行r 仍然等于 L执行r-next L-next把L-next赋成 NULL然后free(L)最后L r-next读的是已经释放的 r 指向的内存。所以 n 必须大于 0这一点靠 2.3 节的入口校验兜住函数内部就不必再判。3.2 成环那一步r-next L-next 与 free(L)为什么不能让头结点留在环里因为头结点的数据域是空的position 和 name 都没有意义。如果把它留在环里报数的时候就会数到一个「没有名字的人」输出名单里也会多出一行乱码。所以头结点只能当脚手架环成型之后必须摘掉。摘掉的方式有讲究。一种写法是r-next L-next; free(L); L r-next;另一种是LinkList first L-next; r-next first; free(L); L first;。两种等价后一种多一个临时变量但把「第一个数据结点」暂存下来读起来意图更明确也不依赖 free 之后 r-next 是否还可靠。严格说free(L) 只回收 L 指向的那块内存r-next 指向的是另一块读它是安全的但初学阶段把顺序理清楚比省一个变量重要。这里可以对照一下不带头结点的版本理解成本会更低。/* 不带头结点的循环链表建法第一个结点要单独处理 */ void CreateLinkListNoHead(LinkList L, int n) { LinkList r NULL, p; int i; L NULL; for (i 1; i n; i) { p (LinkList)malloc(sizeof(LNode)); p-next NULL; p-data.position i; printf(请输入第%d个人员姓名, i); scanf(%7s, p-data.name); if (L NULL) { L p; r p; } /* 第一个结点L 和 r 都指向它 */ else { r-next p; r p; } } if (r ! NULL) r-next L; /* 空表时不能成环必须判空 */ }对比下来能看清头结点的真正价值它把「第一个结点」这个特殊情况从循环里彻底消掉了。不带头结点的版本里那个if (L NULL)判断每插入一次都要执行一遍虽然只命中一次但它让循环体多了一个分支也让「成环」那一步必须加空表判断。题目里说「课下可以创建不带头结点的链表」其实就是在提示这个取舍答辩时能主动对比两种写法说明对链表结构的理解是到位的。3.3 改成不带头结点之后删除函数要跟着动哪几行建表方式一改删除函数就跟着变这一点很多方案里没写清楚导致学生改了一半编译不过。核心差异在于「第一个结点的前驱是谁」带头结点时第一个数据结点的前驱是头结点找前驱的循环从 L 出发走 i-1 步即可不带头结点时第一个数据结点的前驱是最后一个结点必须先把 p 走到尾巴上。具体到代码带头结点版本的while (j i - 1) { p p-next; j; }在不带头结点时不能直接照搬。i 等于 1 时这个循环一步都不走p 停在 L 上而 L 就是第一个结点本身不是它的前驱。这时p-next q-next就变成了「把第一个人自己接给第三个人」第二个人被跳过输出名单直接对不上手算结果。正确做法是在进入循环前加一个特判如果 i 等于 1先while (p-next ! L) p p-next;把 p 推到尾结点再走正常删除流程。这个改动量不大但它揭示了一个规律链表代码里所有跟「第一个元素」相关的分支都是因为循环结构里没有哨兵才冒出来的。加上头结点这类分支全部消失去掉头结点它们就在每个函数里复现一次。所以我的建议是正文主实现保留带头结点的版本把不带头结点的写法放在附录里作为对照两边都跑通答辩时被追问也不慌。4. 出列与删除DeleteLinkList 找前驱、DieLinkList 推进轮次的两段式配合4.1 DeleteLinkList找到第 i-1 个结点才有资格动指针单链表删除的黄金法则只有一句要删第 i 个必须站在第 i-1 个的位置上。因为单链表只能单向走站到第 i 个结点上时已经回不去它的前驱了而没有前驱就改不了前驱-next整个链条就断在这里。/* 删除环中第 i 个结点用 e 带回被删人员的完整信息成功返回 1 */ int DeleteLinkList(LinkList L, int i, ElemType e) { LinkList p L, q; int j 1; if (L NULL || i 1) return 0; /* 空表或非法下标直接退出 */ /* 情况一删的是第一个结点它的前驱是尾结点得先绕一圈找到尾巴 */ if (i 1) { while (p-next ! L) p p-next; /* p 停在最后一个结点上 */ q L; /* q 是要删的第一个结点 */ e q-data; /* 先把数据拷出来 */ p-next q-next; /* 尾结点跨过 q接上新头部 */ L q-next; /* 头指针后移到新的第一个结点 */ free(q); /* 归还内存 */ return 1; } /* 情况二删的是第 i 个i2p 从 L 出发走 i-2 步到第 i-1 个 */ while (j i - 1 p-next ! L) { p p-next; j; } if (p-next L) return 0; /* 走回环入口说明 i 超范围 */ q p-next; /* q 是目标结点 */ e q-data; p-next q-next; /* 前驱跨过 q */ L q-next; /* 下一轮从被删者的下一位开始数 */ free(q); return 1; }几个细节值得逐个说。e q-data必须在 free 之前执行结构体赋值是逐字节拷贝拷完之后 e 就是一份独立副本q 释放与否跟它无关。L q-next这一句是这个题目的特殊要求不是通用链表删除的必备动作因为下一轮报数要从被扔下海的那个人的下一位重新开始数 1所以环的入口必须跟着挪过去。如果漏掉这一句每轮都从固定的位置开始数出列顺序会变成另一套结果跟手算对不上。第二个while循环里加的p-next ! L是防御条件。假如调用方传进来的 i 大于当前环的长度不加这个判断就会在环里无限打转程序看上去卡死实际上是死循环。加上之后走一圈就会碰到环入口直接返回 0调用方能感知到失败。这类防御在课程设计里不是必须但答辩时是个不错的加分点。i 1的分支是原始代码里缺失的那一块也是第 5 章要重点拆的坑。原始版本直接while (j i - 1)i 等于 1 时循环不执行p 停在 L 上然后e p-next-data取到的是第二个人的数据p-next p-next-next把第二个人跨过去等于删错了人。i 等于 2 时同样有问题p 还是停在 L 上L 是第一个人不是前驱逻辑一样偏一位。所以这段特判不是可选项是必须补的。4.2 DieLinkList外层只负责推进轮次不碰指针建环和删除都写完之后主循环反而成了最短的一段。它要做的事情只有三件建一次环、重复调用删除、维护剩余人数。void DieLinkList(LinkList L, int TotalCount, int PersonCount, int LeftTotal) { ElemType e; int LeftCount TotalCount; /* 船上当前还有多少人 */ CreateLinkList(L, TotalCount); /* 建环只做一次放在循环外面 */ printf(抛入大海人员名单\n编号\t姓名\n); /* 每删一个剩余人数减一减到正好等于 LeftTotal 就停 */ while (LeftCount LeftTotal) { if (DeleteLinkList(L, PersonCount, e) 0) { printf(第%d轮删除失败剩余人数异常\n, TotalCount - LeftCount 1); break; } LeftCount--; printf(%d\t%s\n, e.position, e.name); } }LeftCount LeftTotal这个条件是整个程序的关键判断。写成最后会多删一个人写成!一旦 LeftTotal 大于 TotalCount 就永远不成立直接输出空名单。的语义是「还比目标多就继续扔」扔到相等的那一刻退出正好剩 LeftTotal 个人一个字都不差。建环放在循环外面这一点也值得强调。我见过把 CreateLinkList 写进 while 里的版本每轮重新建一次环输一次姓名输入流直接被冲乱程序表现是「明明输入了 5 个名字却一直在问姓名」。原因就是循环次数和建表次数绑在了一起。建环是初始化动作只做一次循环里只做删除。还有一个隐含约束这个循环能正常终止的前提是每轮删除都成功。如果 DeleteLinkList 因为边界问题返回 0 却不 breakLeftCount 就不会减循环条件永远成立程序卡死。所以调用处必须检查返回值这也是把删除函数返回值从 void 改成 int 的实际收益。4.3 DisplayLinkList环的遍历为什么不能判 NULL输出剩余人员是整道题的最后一步也是环形链表和普通单链表差异最大的地方。普通单链表遍历到p NULL结束环形链表里根本没有 NULL照搬这个条件就是一个无限循环屏幕上疯狂刷同一个名字。void DisplayLinkList(LinkList L) { LinkList p L; if (L NULL) { printf(船上已无人生还\n); return; } /* 全被扔下去的情况 */ printf(轮船上剩余人员名单\n编号\t姓名\n); printf(%d\t%s\n, p-data.position, p-data.name); /* 先输出入口结点 */ p p-next; while (p ! L) /* 回到入口就说明走完一圈 */ { printf(%d\t%s\n, p-data.position, p-data.name); p p-next; } }这个写法的核心是「先输出入口结点再从下一个开始走走回入口就停」。之所以不写成 do-while是因为 do-while 在 L 为 NULL 时会先执行一次函数体直接解引用空指针崩溃。先判空再走多一行判断省一类段错误。p ! L这个终止条件依赖一个前提L 必须指向环内的某个有效结点。如果 LeftTotal 等于 0所有人都被删完此时 L 保存的是最后一次删除后q-next的值而那个结点已经在 free(q) 时被释放L 是个悬空指针。拿它去比较虽然不会崩指针比较是合法的但拿它去p-data.position就是读已释放内存。所以 4.2 里的循环结束之后、调用 Display 之前最好再确认一次 LeftCount 大于 0或者在 Display 入口用L NULL兜一层。严格说这个兜底挡不住悬空指针真正的解法是在删除函数里当环被删空时把 L 显式置成 NULL让「空」这件事在类型层面可判断。5. 避坑与排查循环链表里最容易翻车的五个具体位置5.1 报数间隔等于 1 或 2 时前驱定位直接偏一位现象输入 TotalCount5、PersonCount1、LeftTotal2手算应该依次扔下 1、2、3程序输出的却是 2、3、4编号整体后移了一位剩下的是 4 和 5 而不是 4 和 5……再仔细看会发现第一次扔下去的是 2 号。原因删除函数用while (j i - 1)从 L 出发找前驱。i 等于 1 时循环一次都不执行p 停在 L 上而 L 是第一个人本身不是第一个人的前驱。循环链表里第一个人的前驱是最后一个结点位置差了一整圈。i 等于 2 时同理循环还是不执行p 依然是 L取p-next拿到的是第二个人但正确答案要求删的是第二个人前驱应该是第一个人p 站错了地方。解决把 i 等于 1 单独拉一个分支处理先把 p 走到尾结点while (p-next ! L) p p-next;再做指针摘除和 free。i 大于等于 2 时原来的循环逻辑是对的不用动。改完之后用 PersonCount1、2、3 各跑一组三组结果都能跟手算对上才算过。5.2 删除结点只改指针不 free内存一直在漏现象程序运行不报错输出结果也正确但把它放进一个循环里反复跑几百次内存占用会缓慢上涨用内存检查工具跑一遍会报一堆「definitely lost」。原因p-next q-next;之后q 指向的那块堆内存已经没有任何指针引用它了但 malloc 分配的块不会自动回收。原始代码里e p-next-data; L p-next-next; p-next p-next-next;三步走完之后被跳过的那个人就这么悬在堆上谁也找不到谁也释放不了。解决摘链之后立刻free(q);并把 q 置成 NULL防止后续误用。顺序必须是「先取数据、再改指针、最后 free」改指针之前一定要先把q-next存下来或者直接赋给前驱的 next否则 free 之后就再也拿不到下一个结点了。/* 正确的三步存数据 → 改指针 → 释放 */ e q-data; /* 数据先拷出来free 之后就没了 */ p-next q-next; /* 前驱跨过 q环不断 */ L q-next; /* 下一轮入口跟着挪 */ free(q); /* 最后才归还内存 */ q NULL; /* 指针置空避免野指针 */5.3 char name[8] 装不下三个汉字越界改写了 position现象输入「欧阳锋」这类三字姓名输出名单里姓名显示成乱码更诡异的是这个人的 position 变成了一个莫名其妙的数字或者输出顺序整体错乱。原因UTF-8 编码下一个汉字占 3 字节三个字 9 字节加上字符串结束符\0共需 10 字节。name 只有 8 字节scanf写超了多出来的字节顺着结构体内存布局往后覆盖正好压在下一个字段上。结构体里 position 是 int 占 4 字节name 在后或在前取决于编译器对齐但无论哪种布局越界都会污染相邻数据。解决一是把 name 开大16 字节足够放五个汉字二是scanf加宽度限制scanf(%15s, p-data.name)输入超长时截断而不是越界三是如果题目硬性要求 8 字节就在输入提示里明确只接受两字姓名或拼音缩写把约定写进文档。三条里选两条组合即可最稳的是一起做。5.4 LeftTotal 取到两端程序要么空转要么崩现象输入 LeftTotal 等于 TotalCount程序打印完表头就直接跳到剩余名单一个人都没扔输入 LeftTotal 等于 0程序在最后输出剩余名单时直接段错误退出。原因前者是while (LeftCount LeftTotal)一开始就不成立循环体一次都不进逻辑上没错但用户会觉得「怎么什么都没干」后者是所有人被删完L 保存的是最后一次删除后q-next的值而那块内存已经 free 掉了Display 里p-data.position读的是已释放内存属于未定义行为崩不崩看运气。解决LeftTotal 等于 TotalCount 时在 DieLinkList 开头加一句友好提示再直接返回或者干脆不调用删除逻辑直接输出全部LeftTotal 等于 0 时在删除函数的最后一步判断「环是否已空」空了就把 L 置成 NULLDisplay 入口再判一次 NULL 输出「无人生还」。5.5 尾插时写 p-next r-next换个场景就断链现象在原始循环里这段代码跑得好好的一旦把建表函数复制到别的项目、或者把成环那一句挪进循环就在某个位置出现断链环变成一条直线遍历的时候直接走到 NULL 崩掉。原因p-next r-next;在这段代码里成立是因为 r 是当前尾结点而成环之前尾结点的 next 恒为 NULL所以这句等价于p-next NULL。但这个等价关系是特定上下文给的不是语法保证的。一旦成环提前发生r-next 指向的就是环入口新结点会被接到入口上原来的尾结点反而被绕过去环的结构当场破坏。解决新结点的 next 直接写 NULL语义明确不依赖上下文。规范写法是p-next NULL; r-next p; r p;三句各司其职。这个习惯在写任何链表插入代码时都通用尤其是后面要复用到有序插入、中间插入的场景省掉一堆排查时间。6. 验收与答辩三组测试用例加一次五分钟的讲解路线代码跑通不等于方案交付验收这一步得拿出可复现的证据。我一般会准备三组固定用例覆盖正常路径和两个边界跑之前先手算出预期结果写在纸上跑完逐行比对。用例TotalCountPersonCountLeftTotal预期出列顺序预期剩余覆盖点A5323, 1, 52, 4常规路径验证前驱定位与 L 重置B7313, 6, 2, 7, 5, 14多轮推进验证环不散C5121, 2, 34, 5边界专抓 5.1 里那个偏一位用例 A 是手算的主战场从 1 号开始数 1、2、33 号出列下一轮从 4 号数起4、5、1 数到 11 号出列下一轮从 2 号数起2、4、5 数到 55 号出列此时剩 2 和 4 两个人正好等于 LeftTotal停。三行输出加上最后两行剩余合计五行跟程序输出逐字对一遍。用例 B 是经典约瑟夫 n7、k3 的变体网上有现成的手算结果可以交叉验证主要看的是连续删六轮之后环还能不能正常闭合。用例 C 专门打那个 i 等于 1 的分支改之前跑出来是错的改之后对得上这组用例的价值就在于证明第 5 章那个坑确实被填上了。答辩讲解我一般按五分钟来控制顺序是先画三个小图空表带头结点、尾插进行中、成环后头结点被摘掉边画边说 r 指针的作用然后指着 DeleteLinkList 讲「为什么必须站在 i-1 的位置上」顺手说一句 i 等于 1 要特判主动把坑摆出来接着讲L q-next这一句的作用说明为什么下一轮要从被删者的下一位开始数最后报一下复杂度每轮找前驱最多走 PersonCount 步总共删 TotalCount 减 LeftTotal 次整体 O((TotalCount - LeftTotal) × PersonCount)空间只用了 n 个结点O(n)。这套讲法把「结构怎么建、指针怎么动、边界怎么防」三件事串成一条线比逐行念代码有效得多。有一次帮人看一份类似的设计代码本身没问题但答辩时被问「删到只剩一个人的时候L 指向哪儿」当场卡住因为他从来没在小规模数据上停下来观察过指针状态。从那以后我每次交这类带环结构的作业都会强制自己用 n 等于 3、k 等于 2 这种最小规模跑一遍把每一步的 L、p、q 三个指针的值写在纸上跟实际输出对照。这一步花不了十分钟但能把「看着对」和「真的对」区分开答辩时心里也有底。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。