数据结构实验2.3:Josephus问题求解

发布于:2025-04-07 ⋅ 阅读:(21) ⋅ 点赞:(0)


一,问题描述

在现实生活以及计算机科学的一些场景中,我们常常会遇到类似这样的问题:有编号为 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>
#include <malloc.h>

typedef int Status;
typedef struct LNode {
    int no;
    int pass;
    struct LNode* next;
} LNode, * LinkList;

Status CreatJosephus(LinkList& Tail, int n, int M[])    // 创建 Josephus 环(循环单链表)
{
    int i;
    LinkList p, head = NULL;  // 初始化 head 为 NULL
    for (i = 1; i <= n; i++) {
        p = (LinkList)malloc(sizeof(LNode));
        if (p == NULL) {
            return ERROR;
        }
        p->no = i;  // 号码从 1 开始
        p->pass = M[i - 1];
        if (i == 1) {  // 第 1 个结点
            head = p;
            Tail = p;
        }
        else {  // 其余结点
            Tail->next = p;
        }
        Tail = p;
    }
    if (head != NULL) {  // 确保 head 已初始化
        Tail->next = head;  // 令最后一个结点的指针指向第 1 个结点构成循环链表
    }
    return OK;
}

Status Josephus(LinkList& Tail, int m)  // 求解 Josephus 环问题
{
    LinkList q, p, pre;
    int k;  // 用于报数计数器
    if (Tail == NULL || m < 1) {  // 检查链表是否为空和 m 是否合法
        return ERROR;
    }
    p = Tail->next;
    pre = Tail;
    while (p->next != p) {  // 若还有未出列者循环,只剩一个节点时结束
        for (k = 1; k < m; k++) {  // 报数
            pre = p;
            p = p->next;
        }
        printf("%4d", p->no);  // 出列
        q = p;
        m = q->pass;  // 将出列者密码重新赋值给 m
        p = p->next;  // 从出列者下一人开始继续报数
        pre->next = p;  // 修改链指针,从环中删除出列者
        free(q);
    }
    printf("%4d\n", p->no);  // 输出最后一个出列者
    free(p);
    Tail = NULL;
    return OK;
}

void OutputJosephus(LinkList Tail)  // 输出 Josephus 环
{
    if (Tail == NULL) {  // 检查链表是否为空
        return;
    }
    LinkList p = Tail;
    do {
        p = p->next;
        printf("%d(%d),", p->no, p->pass);
    } while (p != Tail);  // 循环条件为未搜索到表尾
    printf("\n");
}

int main() {
    int M[] = { 4, 7, 14, 3, 22, 1, 5, 9, 11, 6, 8, 2 };  // 初始化密码数组
    int n = 12;  // 定义 Josephus 环的长度
    int m = 0;  // 初始密码初始化
    LinkList tail = NULL;
    printf("(1)创建 Josephus 环......\n");
    if (CreatJosephus(tail, n, M)) {
        printf("(2)当前 Josephus 环为:");
        OutputJosephus(tail);
        printf("请输入初始密码:");
        while (m <= 0) {
            scanf("%d", &m);  // 密码必须为正数
        }
        printf("(3)Josephus 环求解结果为:");
        if (!Josephus(tail, m)) {
            printf("求解失败!\n");
        }
    }
    else {
        printf("创建 Josephus 环失败!\n");
    }
    return 0;
}

五,运行效果

1.根据要求调试程序。
在这里插入图片描述

2.运行结果

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到