约瑟夫生者死者问题是数据结构与算法中的经典问题,本文基于单向循环链表这一数据结构实现约瑟夫生者死者游戏,并使用 tkinter技术 实现约瑟夫问题的可视化,现就该问题总结,供学习参考!文末附完整源码资源免费下载~
约瑟夫生者死者游戏的大意是:30个旅客同乘一条船,因为严重超载,加上风高浪大,危险万分;因此船长告诉乘客,只有将全船一半的旅客投入海中,其余人才能幸免遇难。无奈,大家只得同意这种办法,并议定30个人围成一圈,由第一个人开始,依次报数,数到第9人,便把他投入大海中,然后从他的下一个人数起,数到第9人,再将他投入大海,如此循环,直到剩下15个乘客为止。问哪些位置是将被扔下大海的位置。
约瑟夫生者死者游戏的数学建模如下:假设nums个旅客排成一个环形,依次顺序编号1,2,…,nums。从某个指定的第1号开始(第一个开始报数的旅客序号为startNo)为沿环计数,每数到第countNo个人就让其出列,且从下一个人开始重新计数,继续进行下去。这个过程一直进行到剩下endnums个旅客为止。
本游戏的要求用户输入的内容包括:
本游戏要求输出的内容包括:
约瑟夫生者死者游戏主要分为两大模块,分别为 游戏界面展示功能模块与游戏逻辑实现功能模块。 其中,游戏界面包含游戏的主界面、游戏数据录入界面、游戏数据展示界面、游戏介绍界面。在游戏主界面中,用户可以选择开始游戏、查看游戏信息以及退出游戏,若选择开始游戏,则进入游戏数据录入界面。
游戏逻辑实现功能具体包含单向循环链表的实现(数据结构的设计,循环链表的增加、删除、查找功能)、游戏初始化(根据人数初始化单向循环链表的节点个数)、单向循环链表模拟约瑟夫环(通过已经定义好的单向循环链表根据用户传入的参数进行游戏,保存游戏结果)、数据可行性检验(比如输入的人数为负数,间隔数为负数,初始化人数小于希望剩余人数时提供False反馈结果)。
首先进入游戏功能选择界面,用户可以选择开始游戏、游戏介绍以及退出游戏。在游戏介绍界面中,用户可以查看游戏介绍,并可以退出游戏;在游戏功能选择界面中,用户可以选择直接退出游戏。当用户选择开始游戏功能,则进入游戏数据录入界面,用户需要录入开始报数的旅客序号、间隔人数、总人数以及期望剩余人数,如果用户的输入数据合法,则展示游戏结果;反之,则让用户重新输入。在游戏结果展示界面中,用户可以选择是否重新开始游戏,如若是,则重新进入游戏数据输入界面;反之,则退出游戏。
在单向循环链表的设计中,每个节点的信息使用了 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.no
def setNo(self, no):
""" 设置序号 """
self.no = no
def getNext(self):
""" 获取下一节点 """
return self.next
def setNext(self, next):
""" 设置下一节点 """
self.next = next
构建方法封装在 CircleSingleLinkedList 类 中,方法为addPerson(),该方法接收一个参数nums,表示需要初始化单循环链表的节点数量,即初始旅客数量。该类中包含成员属性first,用于指向第一个节点。在构建单向循环链表时需要使用到辅助指针curPerson具体构建思路如下:
构建单向循环链表相关代码:
def addPerson(self, nums):
""" 构建环形链表的方法 """
# 先进行数据校验
if nums < 1:
print("输入人数有误,请重新输入!")
return False
curPerson = None # 辅助指针,用于构建环形链表
# 根据 nums 的值构建
self.personList = []
for i in range(1, nums+1):
# 根据编号,创建节点
person = Person(i)
if i == 1:
# 第一个人,自身成环
self.first = person
self.first.setNext(self.first)
curPerson = self.first
else:
# 新加入一个人,并且再次成环
curPerson.setNext(person)
person.setNext(self.first)
curPerson = person # 更新辅助指针
print("环形链表构建成功!")
return True
在进行游戏模拟时,需要使用到四个参数,均为int类型,分别为startNo(开始报数的旅客序号)、countNo(间隔报数次数)、nums(初始旅客数量)、endnums(期望剩余旅客数量),在进行算法设计时首先应该考虑到参数是否符合逻辑,具体如下:
算法设计定义在CircleSingleLinkedList类中,该类包含四个类属性(均为list类型),分别为personList总人数的序号、liveList按顺序存储存活人的序号、deadList出局人的序号,按照出圈的顺序存储、sortedDeadList用于存储排序后的出局列表。将四个属性设置为类属性方便在算法模拟约瑟夫生者死者游戏后存储相应需要的结果,便于结果展示界面的使用,不必再创建对象。
实现算法的方法名为countPerson(),该方法接收四个参数,分别为分别为startNo(开始报数的旅客序号)、countNo(间隔报数次数)、nums(初始旅客数量)、endnums(期望剩余旅客数量),在算法中首先进行上述的参数合法性检验,如果合法,则进行游戏,并保存结果返回True,主要游戏算法设计步骤如下:
相关代码如下:
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.first
while True:
if helper.getNext() == self.first:
break
else:
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()) # 将出圈的次序和序号存储到deadList
self.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
本模块主要针对用户可能出现的偶然输入,当用户输入汉字二十则传给后台20,即对汉字数字进行处理(简体繁体均可)。与问题的核心算法关联不大,因此只简要介绍,相关处理可以在文章附的资源文件中获得项目完整源码。相关处理结果如下:
中间偏上的位置为游戏名:约瑟夫生者死者游戏,下方是三个按钮。分别是:开始游戏、游戏介绍、退出游戏。
点击开始界面的游戏介绍按钮后会跳转到游戏介绍界面,上方有标题:游戏介绍,下方是游戏介绍的具体内容,以具体实例解释游戏内容。最下方为返回主界面按钮,点击后可返回到主界面。
界面左半部分分别为四个标签及对应的文本输入框,右半部分则是两个按钮,绿色的是进入游戏按钮,橙色的是返回主界面按钮,分别对应各自的功能。输入数据后会由参数处理方法对参数进行处理后传入游戏功能算法。
当用户的输入不合法时,点击进入游戏按钮会弹出错误提示窗口,提示用户输入参数有误,请重新输入,同时清空所有输入框。这里的非法数据主要指非数字汉字以及不合理的数字输入(人数为负数,存活数大于总人数等情况)
输入合法数据后点击进入游戏按钮,进入过程及结果展示页面。左半部分分别放置有两个文本框,功能分别对应各自的标签内容:出局玩家展示(结果经过排序处理)、幸存玩家展示。右半部分为回合展示部分:每一行展示每回合出局的玩家序号(页面可以滑动)。页面最下方有两个按钮,点击重新开始按钮将会进入输入界面,点击退出游戏将会直接退出游戏