一,问题描述
在现实生活以及计算机科学的一些场景中,我们常常会遇到类似这样的问题:有编号为 1,2,…,n 的 n 个人按顺时针方向围坐成一圈,每个人手中持有一个密码(正整数)。此时给定一个随机生成的正整数 m > 0,游戏从编号为 1 的人开始,按照顺时针方向,大家依次从 1 开始顺序报数。当某个人报到 m 时,报数停止,该报 m 的人就要出圈离开。并且,此人出圈时留下他所持有的密码作为新的 m 值。然后,从他在顺时针方向上的下一个人开始,重新从 1 开始报数,如此不断重复这个过程,直到所有的人都全部出列为止。我们的任务就是在计算机上通过编程模拟这个 Josephus 问题的完整求解过程,以便更好地理解和分析其中的规律和逻辑。
二,基本要求
(1)为了准确地模拟 Josephus 环,我们需要使用循环单链表这种数据结构。在这个循环单链表中,每个结点都应该包含三个重要成员:一个用于记录该结点对应人的序号(方便识别是哪个人),一个用于存储该人持有的密码(影响后续报数的 m 值),还有一个指针成员,用于指向下一个结点,从而将所有结点连接成一个环形结构。
(2)设计一个专门的算法来建立一个不带附加表头结点的循环单链表(即 Josephus 环)。这个算法要能够根据输入的人数 n 和每个人的密码,正确地构建出符合要求的循环单链表结构。
(3)精心设计 Josephus 问题的求解算法。该算法要能够按照问题描述的规则,从初始的 m 值开始,通过报数、出圈、更新 m 值等一系列操作,最终实现所有人的出列,并输出相应的出圈顺序。
(4)认真仔细地阅读、理解参考程序中已有的代码逻辑和功能。对于程序中可能存在的不完整部分,要根据前面的要求和算法设计思路,完善相关的代码语句,使其能够正确地实现整个 Josephus 问题的求解过程。
(5)设计一系列合理且全面的测试用例。这些测试用例要涵盖不同的人数 n、不同的初始 m 值以及各种不同的密码组合情况。通过上机运行程序,输入测试用例进行验证,记录下每次运行的测试结果。然后,对这些测试结果进行深入分析,检查程序是否能够正确地处理各种情况,是否存在逻辑错误或者异常情况,以便对程序进行进一步的优化和改进。
三,算法设计
(1)存储结构设计
typedef struct LNode {
// 循环单链表结点类型
int no; // 序号,用于标识该结点对应的是第几个人
int pass; // 密码,会在报数过程中影响 m 值的更新
struct LNode *next; // 指针,用于指向下一个结点,形成循环链表结构
}LNode, *LinkList;
(2)算法设计
① 创建一个不带附加表头结点的循环单链表(Josephus 环):
首先,根据输入的人数 n,使用循环依次为每个人创建一个链表结点。在创建每个结点时,为其分配内存空间,并正确地设置结点的序号和密码。然后,将新创建的结点依次连接到已有的链表中,形成一个循环结构。具体来说,让最后一个结点的指针指向第一个结点,完成循环单链表的构建。
② 令 m 取初始密码、k 计数、p 指向第一个结点、pre 始终指向 p 的前驱,初始时令 pre 指向最后一个结点(表尾指针):
初始化时,将 m 设置为最初给定的随机数(或者根据具体规则确定的初始值)。创建一个变量 k 用于记录当前报数的数值,初始化为 1。设置指针 p 指向循环单链表的第一个结点,作为当前报数的起始位置。同时,设置指针 pre 指向链表的最后一个结点,这样 pre 就始终是 p 的前驱结点,方便在删除结点时进行操作。
③ 用 p 控制循环,每循环到 m 时,输出 p 结点的序号,将 p 结点的密码赋给 m,删除 p 结点,再从 p 的下一结点开始继续循环:
在循环过程中,每次 p 指向一个结点时,k 的值加 1。当 k 的值等于 m 时,说明当前报数到了 m,此时输出 p 结点的序号,表示该人出圈。然后,将 p 结点的密码赋值给 m,更新下一轮报数的终止值。接着,通过修改 pre 的指针,使其指向 p 的下一结点,实现删除 p 结点的操作。最后,将 p 指向 p 的下一结点,重新开始下一轮的报数循环。
④ 重复第 3 步,直至循环链表空为止:
不断重复上述第 3 步的操作,每次都让一个人出圈并更新相关的参数。随着循环的进行,链表中的结点逐渐减少,直到链表为空,即所有人都已经出圈,此时整个 Josephus 问题的求解过程结束。
四,示例代码
#define OK 1
#define ERROR 0
#include <stdio.h>