news 2026/10/17 19:47:16

约瑟夫死亡游戏C语言代码:循环链表实战与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
约瑟夫死亡游戏C语言代码:循环链表实战与避坑指南

简介:这份资源围绕约瑟夫死亡游戏展开,面向正在学习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;如果传 0,r->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的前驱时逻辑仍然成立,但当i=1时p停在L本身,q = p->next就是第二个结点,删除的其实是第二个而不是第一个。这份代码默认PersonCount从 2 开始才有意义,如果用户输入 1,while (j < 0)不执行,p = L,q = 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)在i=1时不执行,p停在L,q指向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为 0,L是野指针。解决:主函数里校验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设为 8,PersonCount设为 3,LeftTotal设为 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-check=full ./josephus

参数说明:-g保留调试符号,--leak-check=full显示详细泄漏点。如果输出里出现definitely lost,说明有malloc没配对free。常见漏点是在DeleteLinkList里只写了p->next = q->next但忘了free(q),或者CreateLinkList里free(L)之后又访问了L->next。

从那以后我每次写完链表删除逻辑,都会先用LeftTotal = 1跑一遍公式对照,再用 Valgrind 扫一遍内存,最后才改参数做多组测试。这套习惯帮我省掉了至少三次深夜调试指针的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 19:44:41

GWR与克里金插值实战:空气质量空间预测从建模到可视化

简介&#xff1a;这份资源是一篇完整的课程论文&#xff0c;围绕地理加权回归&#xff08;GWR&#xff09;模型与克里金插值法在空气质量指数预测中的应用展开&#xff0c;面向地理信息科学、空间统计与遥感方向的学生及研究者&#xff0c;适合具备一定Python与GIS基础、希望掌…

作者头像 李华
网站建设 2026/10/17 19:41:12

PyQt5+SCPI:泰克示波器自动化测试与实时刷新方案

简介&#xff1a;基于PythonPyQt5开发的泰克示波器自动化测试资源&#xff0c;面向需要将泰克MDO3014/3000系列示波器接入上位机进行实时控制与数据采集的测试工程师、自动化脚本开发人员及PyQt5学习者。软件可在界面实时显示示波器波形与图像并保存&#xff0c;同时支持log实时…

作者头像 李华
网站建设 2026/10/17 19:33:41

OJ刷题避坑指南:nbuoj T1141-T1160基础题解与调试技巧

做OJ题解这个系列的时候&#xff0c;我习惯先把一段连续的题号当成一个整体来看&#xff0c;而不是一题一题孤立地刷。nbuoj的T1141到T1160这20道题&#xff0c;老实说不是什么高深莫测的压轴题&#xff0c;但恰恰是这种中前段的题目区间&#xff0c;最能暴露出基础功底的薄弱点…

作者头像 李华
网站建设 2026/10/17 19:31:59

simple_auth鸿蒙适配:OAuth授权与Token刷新改造实战

最近在帮一个跨端项目做鸿蒙适配&#xff0c;项目里 Flutter 部分用到了 simple_auth 这个库。这个库名字起得很谦虚&#xff0c;但实际救过不少命&#xff1a;它把 OAuth 授权、Token 刷新、REST API 请求的认证头注入都封装成了极简的调用接口。起初我以为鸿蒙化只是把 Andro…

作者头像 李华
网站建设 2026/10/17 19:31:38

RAG知识库落地:PDF解析三层实战与踩坑指南

做RAG知识库这一年多&#xff0c;我最大的感受是&#xff1a;最容易被低估的环节不是向量模型&#xff0c;也不是Prompt&#xff0c;而是文档解析&#xff0c;特别是PDF解析。PDF这玩意看起来就是“能复制文字的文档”&#xff0c;可真要把它丢进RAG管线里&#xff0c;问题立刻…

作者头像 李华
网站建设 2026/10/17 19:30:12

商城商品筛选功能全解析:从URL状态同步到索引优化

做商城项目时&#xff0c;最容易被低估的一个需求&#xff0c;就是商品列表页的“过滤条件的筛选”。这个功能乍一看&#xff0c;就是把分类、品牌、价格区间这些条件做成控件&#xff0c;然后带上参数重新请求一遍列表而已。但实际动手之后你会发现&#xff0c;从筛选条件的编…

作者头像 李华