约瑟夫生者死者问题是数据结构与算法中的经典问题,本文基于单向循环链表这一数据结构实现约瑟夫生者死者游戏,并使用 tkinter技术 实现约瑟夫问题的可视化,现就该问题总结,供学习参考!文末附完整源码资源免费下载~


文章目录

  • 1 项目描述
  • 2 系统设计
    • 2.1 系统功能设计
    • 2.2 系统流程设计
  • 3 系统主要功能模块实现
    • 3.1 数据结构的设计与实现
      • 3.1.1 单向循环链表节点设计
      • 3.1.2 单向循环链表的构建
    • 3.2 游戏算法设计与实现
      • 3.2.1 参数合法性检验
      • 3.2.2 游戏算法设计图解
    • 3.3 对汉字数字进行处理*
  • 4 项目实现结果
    • 4.1 游戏主界面
    • 4.2 游戏介绍界面
    • 4.3 数据录入界面
    • 4.4 非法输入处理界面
    • 4.5 过程及结果展示界面
      • 4.5.1 输入数字的结果展示
      • 4.5.2 输入汉字的结果展示
  • 5 项目完整资源

1 项目描述

 约瑟夫生者死者游戏的大意是:30个旅客同乘一条船,因为严重超载,加上风高浪大,危险万分;因此船长告诉乘客,只有将全船一半的旅客投入海中,其余人才能幸免遇难。无奈,大家只得同意这种办法,并议定30个人围成一圈,由第一个人开始,依次报数,数到第9人,便把他投入大海中,然后从他的下一个人数起,数到第9人,再将他投入大海,如此循环,直到剩下15个乘客为止。问哪些位置是将被扔下大海的位置。
 约瑟夫生者死者游戏的数学建模如下:假设nums个旅客排成一个环形,依次顺序编号1,2,…,nums。从某个指定的第1号开始(第一个开始报数的旅客序号为startNo)为沿环计数,每数到第countNo个人就让其出列,且从下一个人开始重新计数,继续进行下去。这个过程一直进行到剩下endnums个旅客为止。
本游戏的要求用户输入的内容包括:

  1. 旅客的个数,即 初始人数,也就是nums的值;
  2. 离开旅客的 间隔数,即每间隔多少个数字出列旅客,也就是countNo的值;
  3. 从序号几开始计数,即指定 第一个数数旅客的序号,记为startNo;
  4. 指定期望 最终剩余旅客的数量,记为endnums。

本游戏要求输出的内容包括:

  1. 离开旅客的序号;
  2. 剩余旅客的序号
  3. 每轮出局旅客的中间结果。

2 系统设计

2.1 系统功能设计

 约瑟夫生者死者游戏主要分为两大模块,分别为 游戏界面展示功能模块与游戏逻辑实现功能模块。 其中,游戏界面包含游戏的主界面、游戏数据录入界面、游戏数据展示界面、游戏介绍界面。在游戏主界面中,用户可以选择开始游戏、查看游戏信息以及退出游戏,若选择开始游戏,则进入游戏数据录入界面。
 游戏逻辑实现功能具体包含单向循环链表的实现(数据结构的设计,循环链表的增加、删除、查找功能)、游戏初始化(根据人数初始化单向循环链表的节点个数)、单向循环链表模拟约瑟夫环(通过已经定义好的单向循环链表根据用户传入的参数进行游戏,保存游戏结果)、数据可行性检验(比如输入的人数为负数,间隔数为负数,初始化人数小于希望剩余人数时提供False反馈结果)。

2.2 系统流程设计

 首先进入游戏功能选择界面,用户可以选择开始游戏、游戏介绍以及退出游戏。在游戏介绍界面中,用户可以查看游戏介绍,并可以退出游戏;在游戏功能选择界面中,用户可以选择直接退出游戏。当用户选择开始游戏功能,则进入游戏数据录入界面,用户需要录入开始报数的旅客序号、间隔人数、总人数以及期望剩余人数,如果用户的输入数据合法,则展示游戏结果;反之,则让用户重新输入。在游戏结果展示界面中,用户可以选择是否重新开始游戏,如若是,则重新进入游戏数据输入界面;反之,则退出游戏。


3 系统主要功能模块实现

3.1 数据结构的设计与实现

3.1.1 单向循环链表节点设计

 在单向循环链表的设计中,每个节点的信息使用了 Person类 来定义,节点包含no与next 两个属性,分别表示每个人的编号与下一个节点的位置信息。在Person类中定义了setNo()、getNo()、setNext()、getNext()方法,分别用于设置序号、获取序号、设置下一个节点、获取下一个节点。

Person类代码:

class Person:""" Person类,表示一个节点 """def __init__(self, no, next=None):""" Person类属性相关 """self.no = no# 每个人的编号,int类型self.next = next# 指向下面一个节点,默认None,Person类型def getNo(self):""" 获取序号 """return self.nodef setNo(self, no):""" 设置序号 """self.no = nodef getNext(self):""" 获取下一节点 """return self.nextdef setNext(self, next):""" 设置下一节点 """self.next = next

3.1.2 单向循环链表的构建

 构建方法封装在 CircleSingleLinkedList 类 中,方法为addPerson(),该方法接收一个参数nums,表示需要初始化单循环链表的节点数量,即初始旅客数量。该类中包含成员属性first,用于指向第一个节点。在构建单向循环链表时需要使用到辅助指针curPerson具体构建思路如下:

  1. 当nums < 1时:提示输入人数有误,请重新输入!
  2. 当nums = 1时:通过 Person类的__init__()方法,新建一个节点person,并让first指向 person,person节点的next指向自身,即此时first指向指向的位置,自身成环,节点的no赋值为1;
  3. 当nums > 1时:新创建一个节点person,先让辅助指针curPerson(此时指向原单向循环链表的第一个节点,即first指向的节点)指向的节点的next指向新节点person,再让新节点person的next指向first重新构建环形链表,最后更新辅助指针curPerson指向person。添加节点具体个数由参数nums而定,进行nums次循环,重复上述步骤。

构建单向循环链表相关代码:

def addPerson(self, nums):""" 构建环形链表的方法 """# 先进行数据校验if nums < 1:print("输入人数有误,请重新输入!")return FalsecurPerson = None# 辅助指针,用于构建环形链表# 根据 nums 的值构建self.personList = []for i in range(1, nums+1):# 根据编号,创建节点person = Person(i)if i == 1:# 第一个人,自身成环self.first = personself.first.setNext(self.first)curPerson = self.firstelse:# 新加入一个人,并且再次成环curPerson.setNext(person)person.setNext(self.first)curPerson = person# 更新辅助指针print("环形链表构建成功!")return True

3.2 游戏算法设计与实现

3.2.1 参数合法性检验

 在进行游戏模拟时,需要使用到四个参数,均为int类型,分别为startNo(开始报数的旅客序号)、countNo(间隔报数次数)、nums(初始旅客数量)、endnums(期望剩余旅客数量),在进行算法设计时首先应该考虑到参数是否符合逻辑,具体如下:

  1. 开始报数的旅客序号startNo < 1:不合法,返回False;
  2. 开始报数的旅客序号startNo > nums:不合法,返回False;
  3. 初始旅客数量nums < 期望剩余旅客数endnums:不合法,返回False;
  4. 间隔报数次数countNo <= 0:不合法,返回False。

3.2.2 游戏算法设计图解

 算法设计定义在CircleSingleLinkedList类中,该类包含四个类属性(均为list类型),分别为personList总人数的序号、liveList按顺序存储存活人的序号、deadList出局人的序号,按照出圈的顺序存储、sortedDeadList用于存储排序后的出局列表。将四个属性设置为类属性方便在算法模拟约瑟夫生者死者游戏后存储相应需要的结果,便于结果展示界面的使用,不必再创建对象。
 实现算法的方法名为countPerson(),该方法接收四个参数,分别为分别为startNo(开始报数的旅客序号)、countNo(间隔报数次数)、nums(初始旅客数量)、endnums(期望剩余旅客数量),在算法中首先进行上述的参数合法性检验,如果合法,则进行游戏,并保存结果返回True,主要游戏算法设计步骤如下:

  1. 初始化循环链表: 先根据参数nums,调用类中的addPerson()方法初始化循环链表,这里以nums=6为例,first初始指向序号为1的节点,构建好的循环链表如图所示。
  2. 移动辅助指针helper: 在算法设计中需要使用到辅助指针helper,其初始
    位置为first指向的节点前一个,即序号为6的节点;
  3. 根据开始序号移动辅助指针helper和first: 根据startNo,对helper和first两个指针进行移动startNo-1次,这里以startNo=3为例,helper和first应该移动两次,分别位于序号2和3处,表示以3为开始序号报数;
  4. 模拟约瑟夫生者死者游戏的出局: 先定义一个死循环,当nums=endnums时,即人数减少到期望剩余人数时结束循环,否则对人数nums进行递减,每次循环让first与helper两个指针移动countNo-1次,并将first指针指向的节点从单向循环链表中进行删除,重复移动first与helper,直到符合条件退出循环。这里以endnums=4、countNo=2为例,即每报数两个出局一个,最终剩下四个人,示意图如下:

  5. 将出局的结果和幸存的结果保存在类属性中,方便界面输出端使用。最终幸存旅客序号为1,2,3,5;出局旅客序号为4,6。

相关代码如下:

class CircleSingleLinkedList:""" 算法实现 """# 用于存储需要的结果集,方便调用personList = []# 总人数序号liveList = []# 类变量,按顺序存储存活人序号deadList = []# 出圈的序号,按出圈顺序存储sortedDeadList = []# 排序后的出局列表def __init__(self, first=None):self.first = first# 指向第一个节点def countPerson(self, startNo, countNo, nums, endnums):"""调用构造环链表方法并计算出圈次序, 抛入大海的次序方法, 同时将结果存储在类变量中方便使用@:param startNo 从序号startNo的人开始数@:param countNo 数多少次@:param nums 最初圈的人数@:param endnums 希望最终剩下的人数@:return True 计算成功, False 计算失败,可能参数有误"""# 先对数据进行校验if startNo < 1 or startNo > nums or nums < endnums or countNo <= 0:print("参数输入有误!")return False# 对类属性进行重置CircleSingleLinkedList.personList = [i for i in range(1, nums + 1)]CircleSingleLinkedList.liveList = []CircleSingleLinkedList.deadList = []# 初始化链表self.addPerson(nums)# 利用辅助指针,完成出圈的操作,helper最初指向环形链表的最后一个节点helper = self.firstwhile True:if helper.getNext() == self.first:breakelse:helper = helper.getNext()# 报数前,先将first与helper移动startNo-1次for i in range(0, startNo - 1):self.first = self.first.getNext()helper = helper.getNext()# 报数出圈while True:if nums == endnums:# 结束条件,当人数减少到期望剩余人数时break# helper 与 first 同时移动 countNo - 1次for i in range(0, countNo - 1):self.first = self.first.getNext()helper = helper.getNext()# first指向的出局print("出圈的序号 %d" % self.first.getNo())CircleSingleLinkedList.deadList.append(self.first.getNo())# 将出圈的次序和序号存储到deadListself.first = self.first.getNext()helper.setNext(self.first)nums = nums - 1# 存储最终存活人的结果CircleSingleLinkedList.liveList=[i for i in CircleSingleLinkedList.personList if i not in CircleSingleLinkedList.deadList]CircleSingleLinkedList.sortedDeadList = CircleSingleLinkedList.deadList.copy()CircleSingleLinkedList.sortedDeadList.sort()return True

3.3 对汉字数字进行处理*

本模块主要针对用户可能出现的偶然输入,当用户输入汉字二十则传给后台20,即对汉字数字进行处理(简体繁体均可)。与问题的核心算法关联不大,因此只简要介绍,相关处理可以在文章附的资源文件中获得项目完整源码。相关处理结果如下:


4 项目实现结果

4.1 游戏主界面

 中间偏上的位置为游戏名:约瑟夫生者死者游戏,下方是三个按钮。分别是:开始游戏、游戏介绍、退出游戏。

4.2 游戏介绍界面

 点击开始界面的游戏介绍按钮后会跳转到游戏介绍界面,上方有标题:游戏介绍,下方是游戏介绍的具体内容,以具体实例解释游戏内容。最下方为返回主界面按钮,点击后可返回到主界面。

4.3 数据录入界面

 界面左半部分分别为四个标签及对应的文本输入框,右半部分则是两个按钮,绿色的是进入游戏按钮,橙色的是返回主界面按钮,分别对应各自的功能。输入数据后会由参数处理方法对参数进行处理后传入游戏功能算法。

4.4 非法输入处理界面

 当用户的输入不合法时,点击进入游戏按钮会弹出错误提示窗口,提示用户输入参数有误,请重新输入,同时清空所有输入框。这里的非法数据主要指非数字汉字以及不合理的数字输入(人数为负数,存活数大于总人数等情况)

4.5 过程及结果展示界面

 输入合法数据后点击进入游戏按钮,进入过程及结果展示页面。左半部分分别放置有两个文本框,功能分别对应各自的标签内容:出局玩家展示(结果经过排序处理)、幸存玩家展示。右半部分为回合展示部分:每一行展示每回合出局的玩家序号(页面可以滑动)。页面最下方有两个按钮,点击重新开始按钮将会进入输入界面,点击退出游戏将会直接退出游戏

4.5.1 输入数字的结果展示


4.5.2 输入汉字的结果展示



5 项目完整资源

完整源码资源链接,供交流学习使用:
约瑟夫生者死者游戏-Python数据结构与算法实战项目完整源码与界面资源