
简介这份资源围绕约瑟夫死亡游戏展开面向正在学习C语言数据结构与算法的大学生及自学者帮助理解循环链表在实际问题中的建模与操作。压缩包内仅含1个docx文档约18KB以文字形式完整呈现设计思路、算法逻辑与代码实现便于边读边对照练习。内容从元素类型ElemType与结点类型LNode的定义讲起逐步展开带头结点循环链表的创建、按指定间隔删除结点、重新设置头指针以及输出剩余人员等关键环节并给出CreateLinkList、DeleteLinkList、DieLinkList、DisplayLinkList等函数的完整代码与注释清晰体现数到第PersonCount人即抛入大海、直至剩余LeftTotal人的整体流程。目前已有3143人学习适合作为课程实验、链表专题练习或算法入门的参考材料也可据此自行改写为不带头结点版本以加深理解。1. 约瑟夫死亡游戏 C 语言代码一份能直接跑通的循环链表实战包约瑟夫环是数据结构课里绕不开的经典模型但多数人只停留在“知道公式”的层面真让你用 C 语言手写一个带头结点、能动态删人、还能打印幸存者名单的完整版本翻车概率不低。这份资源给的就是一套能直接编译运行的约瑟夫死亡游戏 C 语言代码核心是循环链表每个结点存编号和姓名数到指定人数就摘掉该结点并释放内存直到剩下安全人数为止。它适合两类人——正在啃链表指针操作的学生以及想拿一个“指针边界清晰、内存释放完整”的小项目练手的开发者。代码不长但把结构体定义、尾插建环、定位删除、头指针重设这几个关键动作全串起来了比单纯看教材伪代码有用得多。2. 拆解代码骨架结构体、结点与链表类型的三层定义2.1 为什么元素类型和结点类型要分开定义这份代码最值得先看的是类型定义的分层。很多初学者写链表时习惯把数据和指针塞进同一个结构体然后直接用这个结构体当链表类型结果在删除、遍历、传参时到处是-next-next这种难维护的写法。这里把ElemType和LNode拆开逻辑上更接近实际工程里的做法。typedef struct ElemType { // 元素类型只关心业务数据 int position; // 编号从 1 开始 char name[8]; // 姓名定长数组注意长度限制 } ElemType; typedef struct LNode { // 结点类型数据域 指针域 ElemType data; struct LNode *next; } LNode; typedef LNode *LinkList; // 链表类型本质是结点指针ElemType里name[8]是个硬约束意味着姓名最多 7 个字符加一个结束符。如果你输入“张三丰”没问题输入“欧阳锋”也没问题但输入超过 7 个字节的中文名就会越界。常见做法是把长度改成 32 或 64或者改用char *动态分配。LinkList被定义为LNode *所以后面所有函数参数里的LinkList L实际上是指针的引用这是 C 写法如果你用纯 C 编译器需要改成LinkList *L并相应调整调用处的取地址符。这一点在混合编译环境里经常被忽略导致报错“expected ‘;’ before ‘’”。2.2 带头结点建环尾插法加一步成环创建链表的部分用了带头结点的尾插法最后释放头结点并把L指向第一个真实结点。这个套路在单链表里很常见但成环的那一步是约瑟夫环的关键。void CreateLinkList(LinkList L, int n) { LinkList r, p; int i; L (LinkList)malloc(sizeof(LNode)); // 头结点 L-next NULL; r L; // r 永远指向尾结点 for (i 1; i n; i) { p (LinkList)malloc(sizeof(LNode)); p-next NULL; p-data.position i; printf(请输入%d 个人员姓名, i); scanf(%s, p-data.name); r-next p; // 新结点挂在尾部 r p; // 尾指针后移 } r-next L-next; // 尾结点指向第一个真实结点成环 free(L); // 释放头结点 L r-next; // L 指向环的入口 }逻辑说明头结点只是临时占位真正参与游戏的是L-next开始的 n 个结点。循环结束后r停在最后一个结点让r-next指向第一个真实结点环就闭合了。然后free(L)释放头结点L r-next让链表指针落到第一个真实结点上。参数n是总人数必须大于 0如果传 0r-next L-next会让尾结点指向 NULL后面遍历直接崩。我一般会在函数入口加一句if (n 0) return;做防御。注意free(L)之后不能再访问原来的头结点L r-next必须在free之前或之后立即执行顺序不能乱。如果先L r-next再free(L)就把第一个真实结点释放了。3. 删除第 PersonCount 个结点定位、摘链、重设头指针3.1 删除函数的指针操作与边界DeleteLinkList负责数到指定人数后把对应结点摘掉并返回被删人员信息。它的核心是找到第i-1个结点然后操作p-next。void DeleteLinkList(LinkList L, int i, ElemType e) { LinkList p, q; p L; int j 1; while (j i - 1) { // 停在目标结点的前一个位置 p p-next; j; } q p-next; // q 指向要删除的结点 e q-data; // 保存被删数据 L q-next; // 头指针先移到下一个结点 p-next q-next; // 前驱跳过 q free(q); // 释放内存 }这里有一个容易翻车的点L q-next和p-next q-next的顺序。如果先执行p-next q-next再执行L q-next当p就是L的前驱时逻辑仍然成立但当i1时p停在L本身q p-next就是第二个结点删除的其实是第二个而不是第一个。这份代码默认PersonCount从 2 开始才有意义如果用户输入 1while (j 0)不执行p Lq L-next删掉的是第二个人。常见做法是在调用前把PersonCount规范到[2, TotalCount]区间或者在删除函数里单独处理i 1的情况。3.2 循环删除直到剩余 LeftCount 人DieLinkList把创建、删除、打印串成主流程。它用LeftCount记录当前剩余人数每删一个减一直到等于LeftTotal。void DieLinkList(LinkList L, int TotalCount, int PersonCount, int LeftTotal) { ElemType e; int LeftCount TotalCount; CreateLinkList(L, TotalCount); printf(跑到大海中人员名单\n 编号\t 姓名\n); while (LeftCount LeftTotal) { DeleteLinkList(L, PersonCount, e); LeftCount--; printf(%d\t%s\n, e.position, e.name); } }参数说明TotalCount是总人数PersonCount是每次数到几就扔人LeftTotal是希望剩下的人数。循环条件是LeftCount LeftTotal意味着当剩余人数降到LeftTotal时停止。如果LeftTotal大于TotalCount循环一次都不执行直接打印空名单如果LeftTotal为 0所有人都会被删完最后L变成野指针DisplayLinkList会访问非法内存。我一般会在主函数里加校验LeftTotal必须小于TotalCount且大于 0。提示DeleteLinkList里没有释放q之后把q置空虽然函数返回后q是局部变量不影响外部但在调试时如果打印q会看到悬空地址。养成free(q); q NULL;的习惯没坏处。4. 避坑与排查指针链表最容易翻车的五个地方4.1 输入姓名超过 7 字节导致栈溢出现象输入较长姓名后程序不报错但后续打印乱码或者直接崩溃。原因char name[8]只能存 7 个字符加结束符scanf(%s)不检查长度。解决把name改成char name[32]或者用scanf(%7s, p-data.name)限制宽度。更稳妥的做法是用fgets并手动去掉换行符。4.2 PersonCount 输入 1 时删除逻辑错位现象想删第 1 个人结果第 2 个人被扔下海。原因DeleteLinkList的while (j i - 1)在i1时不执行p停在Lq指向L-next。解决在DieLinkList里加判断如果PersonCount 1就把它当作TotalCount处理或者单独写一个删除头结点的分支。4.3 释放头结点后 L 未及时重设现象创建链表后第一次删除就崩溃报段错误。原因CreateLinkList里free(L)之后如果L r-next没执行或执行顺序错误L指向已释放内存。解决确保free(L)和L r-next紧挨着且r-next在free之前已经指向第一个真实结点。4.4 LeftTotal 等于 TotalCount 时打印空环现象程序输出“轮船上剩余人员名单”后没有任何人员。原因while (LeftCount LeftTotal)不执行L仍然是创建后的完整环但DisplayLinkList从L开始打印逻辑上应该能打印所有人。实际翻车点在于DieLinkList里CreateLinkList之后没有检查L是否为空。如果TotalCount为 0L是野指针。解决主函数里校验TotalCount 0且LeftTotal TotalCount。4.5 纯 C 编译器不识别引用符号 现象用 GCC 编译报错expected ;, , or ) before token。原因LinkList L是 C 引用语法C 语言不支持。解决把函数参数改成LinkList *L函数内用(*L)访问调用处用L传地址。或者直接把文件后缀改成.cpp用 C 编译器编译。5. 验证与进阶用随机姓名批量测试和公式对照5.1 批量生成测试数据验证删除顺序手动输入姓名太慢我一般会写一个测试桩用随机姓名和固定参数跑多组检查删除顺序是否符合约瑟夫环的数学递推。下面这段代码可以临时替换CreateLinkList里的scanf部分自动填充姓名。// 在 CreateLinkList 的 for 循环内替换 scanf 行 const char *names[] {A, B, C, D, E, F, G, H}; p-data.position i; strcpy(p-data.name, names[(i - 1) % 8]);逻辑说明用固定数组循环取名字避免手动输入。参数TotalCount设为 8PersonCount设为 3LeftTotal设为 1跑出来的删除顺序应该是 3、6、1、5、2、8、4最后剩下 7。这个结果可以用约瑟夫环递推公式f(n,m) (f(n-1,m) m) % n验证。如果删除顺序对不上优先检查DeleteLinkList里j的初始值和while条件。5.2 用递归公式反推验证代码正确性约瑟夫环的数学解是J(n, m) (J(n-1, m) m) % n其中J(1, m) 0。把编号从 0 开始算最后加 1 就是幸存者编号。你可以写一个十行的递归函数算出理论幸存者再和DisplayLinkList打印的结果对照。int josephus(int n, int m) { if (n 1) return 0; return (josephus(n - 1, m) m) % n; } // 调用printf(理论幸存者编号%d\n, josephus(TotalCount, PersonCount) 1);注意这个公式只适用于LeftTotal 1的情况。如果LeftTotal大于 1需要把删除过程完整模拟出来不能直接用公式。我一般会先用LeftTotal 1验证代码的删除逻辑再改LeftTotal观察剩余名单是否合理。5.3 内存泄漏检查与 Valgrind 快速跑法链表代码最容易出的问题是malloc之后忘了free。这份代码在DeleteLinkList里释放了被删结点但CreateLinkList里释放了头结点整体是平衡的。如果你改了代码比如在DisplayLinkList里临时申请了数组却没释放可以用 Valgrind 跑一遍。gcc -g -o josephus josephus.c valgrind --leak-checkfull ./josephus参数说明-g保留调试符号--leak-checkfull显示详细泄漏点。如果输出里出现definitely lost说明有malloc没配对free。常见漏点是在DeleteLinkList里只写了p-next q-next但忘了free(q)或者CreateLinkList里free(L)之后又访问了L-next。从那以后我每次写完链表删除逻辑都会先用LeftTotal 1跑一遍公式对照再用 Valgrind 扫一遍内存最后才改参数做多组测试。这套习惯帮我省掉了至少三次深夜调试指针的后悔药。希望帮到你。本文还有配套的精品资源点击获取