目录
1问题描述及需求分析
1.1问题描述
1.2相关文献资料
1.3需求分析
2 总体设计
2.1 算法设计思路
2.2总设计图
2.3 各函数之间的调用关系
3 详细设计
3.1相关数据定义
3.2各函数的功能设计
4 系统测试及结果
4.1测试用例的设计
4.2 测试结果记录和分析
5 课程设计总结
参考文献
附录 源程序
约瑟夫生死者游戏:n名旅客同乘一条船,因为严重超载,加上风高浪大,危险万分;因此船长告诉旅客,只有将全船一半的旅客投入海中,其余人才能幸免遇难。无奈,大家只得同意这种办法,并商议决定n个人围成一圈,由第一个人开始,依次报数,数到第k个人,便把他投入大海中,然后从他的下一个人重新开始报数,数到第k人,再将他投入大海,如此循环,直到剩下n/2个旅客为止。问哪些位置是将被扔下大海的位置。
约瑟夫生死者游戏有许多描述的方式,此问题的来历也有很多。其中与本题目比较相似的有17世纪的法国数学家加斯帕在《数目的游戏问题》中讲的一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。利用C语言程序设计模拟实现约瑟夫生死者游戏的方法也有很多种,其中主要有数组、循环链表、递归三种实现方法。此次课设约瑟夫生死者游戏主要由单向循环链表的方法进行求解。
(1)模拟上述约瑟夫生死者游戏的进行过程。
(2)构造一个单向循环链表函数。由用户输入n(旅客总人数)值,创建一个具有n个有效节点的单向循环链表。
(3)编写一个输出单向循环链表中各个有效节点编号值的函数,可将旅客编号按照顺序依次输出。
(4)编写实现约瑟夫生死者游戏功能的函数。其中包含根据用户输入的最大报数值k,将满足条件的旅客编号从循环链表中删除的操作。
(5)在完成一次约瑟夫生死者游戏以后,需要一个将创建的循环链表释放的函数。
(6)创建一个菜单使用户可以循环执行该游戏并让输入界面与输出界面简洁美观,使用户可以清楚如何运行该游戏。
(7)保证程序的可读性与健壮性较强。
本游戏的数学建模如下:假设n名旅客排成一个环形,依次将所有的旅客按顺序编号1,2,…,n。从1号开始,沿环计数,数到第k个就让其出列,然后从k+1个人重新沿环计数,数到第k个就让其出列。这个过程一直进行到剩下一半的旅客为止。
本游戏要求用户输入的内容包括:
(1)船上旅客总人数,也就是n的值。
(2)计数的最大值,也就是k的值。
本游戏要求输出的内容包括:
(1)初始船上所有旅客的编号。
(2)投入大海中的旅客的编号。
(3)幸存下来的旅客的编号。
根据以上对模型的分析和输入值输出值的参数分析,可以定义循环链表的数据结构进行算法实现。
首先定义一个结构体,里面包含一个数据域(存放编号值)和一个指针域。
根据需求,用户需输入n与k的值,定义一个initvalue的函数,将用户输入的n与k的值保存下来。
编写CreateCLlist函数。建立一个单向循环链表,并且在建立循环链表的过程中将各个旅客的编号存入结构体的数据域中。
编写display函数。将循环链表里所有节点的编号依次按照顺序输出。
编写Josephring函数。实现投入海中的旅客和幸存的旅客的选择。此函数为该问题的核心,即程序中最关键的部分。核心算法需要借助链表的循环,通过while循环与for循环的结合,利用指针的移动来定位要删除的节点,并且将其删除。
编写FreeCLlist函数。在本次游戏进行完毕时,删除建立的循环链表,释放空间,避免在多次运行该游戏后占用太大的空间。
设计交互页面,建立一个较为美观的菜单。利用while循环语句与if条件语句实现用户对开始游戏,重新开始与退出游戏的选择。
将进行游戏用到的所有函数放入Gamestart函数中,方便用户根据菜单进行选择来调用函数。
建立一个头文件,将编写的函数放入头文件中,在主函数中调用该头文件。保证所有代码的可读性较强,使得代码清晰易懂。
void initvalue(int *sum,int *count)函数,获取用户输入的sum和count值,得到船上旅客总人数与报数最大值。
struct JosephNode * CreateCLlist(int sum)函数,创建单向循环链表并且给所有旅客编号;
void display(PNode head)函数,按照编号顺序,输出循环链表内剩余所有节点的编号;
void Josephring(PNode head,int sum,int count)函数,进行约瑟夫游戏,删除符合报数条件的节点;
void FreeCLlist(PNode head)函数,释放单向循环链表;
void Gamestart()函数,调用以上进行约瑟夫游戏的所有函数;
void JosephGame_menu()函数,菜单让用户了解游戏的玩法。如何开始游戏、重新开始以及退出游戏;
各函数之间的调用关系如表2-3所示。
表2-3 各函数之间调用关系
*主调函数* | *被调函数* |
---|---|
void initvalue(int *sum,int *count) | 无 |
struct JosephNode * CreateCLlist(int sum) | 无 |
void display(PNode head) | 无 |
void Josephring(PNode head,int sum,int count) | 无 |
void FreeCLlist(PNode head) | 无 |
void Gamestart() | 以上所有 |
void JosephGame_menu() | 无 |
int main() | 以上所有 |
变量的命名规则就是直接写出相关变量的英文,见名知意,增加代码的可读性。相关的数据类型,变量含义见表3-1。
约瑟夫结构体定义:
typedef struct JosephNode
{
int id;
struct JosephNode *next;
}Node,*PNode;
*变量名* | *说明* | *类型* |
---|---|---|
sum | 旅客总人数 | int |
count | 报数最大值 | int |
id | 旅客编号 | int |
x | 菜单选择变量 | int |
dead | 剩余应投海人数 | int |
rear | 结构体指针 | PNode |
pf | 结构体指针 | PNode |
ph | 结构体指针 | PNode |
head | 结构体指针 | PNode |
表3-1 相关数据的定义和说明
(1)void initvalue(int *sum,int *count)
功能描述:定义初始时船上旅客总人数以及报数最大值。将sum与count的地址传入函数中,在本函数中完成对sum与count的赋值。
参数含义:sum、count为指向旅客总人数和报数最大值的指针。
返回值含义:函数无返回值。
函数功能截图:如图3-2-1所示。
图3-2-1 initvalue函数功能截图
函数程序流程图:如图3-2-2所示。
图3-2-2 initvalue函数程序流程图
(2)PNode CreateCLlist(int sum)
功能描述:创建单向循环链表。
参数含义:sum为船上旅客总人数。
返回值含义:返回一个指向结构体的指针head,head指向所创建单向循环链表的头节点。
函数功能截图:无。
函数程序流程图:如图3-2-3所示。
图3-2-3 CreateCLlist函数程序流程图
(3)void display(PNode head)
功能描述:输出单向循环链表内所有节点的编号值。定义一个指针p指向head->next,利用指针的移动,依次输出各个节点的id值。需要注意的是循环进行的条件,因为此链表是单向循环链表,所以循环进行的条件不再是p->next!=NULL,而是当p!=head时,输出p->id,若p==head则终止循环。
参数含义:head为指向循环链表头节点的指针。
返回值含义:函数无返回值。
函数功能截图:如图3-2-4所示。
图3-2-4 display函数功能截图
函数程序流程图:如图3-2-5所示。
图3-2-5 display函数程序流程图
(4)void Josephring(PNode head,int sum,int count)
功能描述:找到符合报数条件的节点位置并将其删除。此函数为整个程序中最重要的函数。首先定义两个指针,开始时,一个指针指向头节点,一个指向第一个有效节点。开始进行报数,每报一次数,两个指针各向前移动一位,如果前面的指针指向了头节点,那么两个指针再各向前移动一位。达到报数最大值时,前面的指针就指向了要删除的节点,对其进行删除即可。然后开始新的一轮报数,直到链表剩下一半有效节点为止。
参数含义:head为指向循环链表头节点的结构体指针,pf与ph都为结构体指针,pf的指向为ph指向的前一个节点,sum为旅客总人数,count为报数最大值,dead为剩余应投海的人数。
函数功能截图:如图3-2-6所示。
图3-2-6 Josephring函数功能截图
函数程序流程图:如图3-2-7所示。
图3-2-7 Josephring函数程序流程图
(5)void FreeCLlist(PNode head)
功能描述:释放所建立的单向循环链表。释放单向循环链表与释放单链表不同,单链表释放可以直接利用双指针从头节点开始,依次释放直到前面的指针指空即释放完成。而单向循环链表首先要将头节点与后面的节点断开,再由head与p两个指针依次释放所有节点。
参数含义:head为指向循环链表头节点的结构体指针,p为结构体指针。
函数功能截图:输出“游戏结束”即完成了循环链表的释放,如图3-2-8。
图3-2-8 FreeCLlist函数功能截图
函数程序流程图:如图3-2-9所示。
图3-2-9 FreeCLlist函数程序流程图
(6)void JosephGame_menu()
功能描述:提示用户本游戏如何开始游戏、重新开始、退出游戏。
参数含义:无参数。
函数功能截图:如图3-2-10。
图3-2-10 JosephGame_menu函数功能截图
函数程序流程图:无。
(7)void Gamestart()
功能描述:调用进行游戏需要的所有函数。方便用户重新开始游戏。
参数含义:head为指向循环链表头节点的结构体指针,sum为旅客总人数,count为报数最大值。
函数功能截图:如图3-2-11。
函数程序流程图:如图3-2-12。
图3-2-11 Gamestart函数功能截图
图3-2-12 Gamestart函数程序流程图
被测函数/模块 | 用例编号 | 测试目的 | 输入数据 | 预期结果 |
---|---|---|---|---|
CreateCLlist | 1 | 检测该函数是否正确创建出来了单向循环链表 | sum=10 | 1,2,3,4,5,6,7,8,9,10 |
display | 1 | 检测该函数是否正确输出了剩余节点的id值 | sum=10 | 1,2,3,4,5,6,7,8,9,10 |
Josephring | 2 | 检测该函数是否删除了符合报数条件的节点 | sum=10count=3 | 1,4,5,8,10 |
FreeCLlist | 3 | 检测该函数是否正确释放循环链表的剩余节点 | sum=10 | 输出“游戏结束” |
Gamestart | 4 | 检测该函数是否能正确的调用运行游戏的函数 | sum=10count=3 | 正确输出需要的各个数据 |
表4-1 测试用例的设计
用例编号:1
期望结果:1,2,3,4,5,6,7,8,9,10
测试代码截图:如图4-2-1。
运行结果:输出错误,如图4-2-2。
图4-2-1 测试用例1代码截图
图4-2-2 测试用例1运行结果
结果分析:CreateCLlist函数没有出现问题,display函数在p指针的起始指向上与while循环的结束条件上出现了问题。一开始应该将p指针指向head->next,while循环的结束条件改为p!=head。这样既保证了能进入while循环又可以将链表完全遍历。如果一开始p=head的话应该使用do-while语句,先将p后移一位打印出p->id,再判断p->next!=head,将链表完全遍历。
用例编号:2
期望结果:1,4,5,8,10
测试代码截图:如图4-2-3。
运行结果:输出错误,如图4-2-4。
图4-2-3 测试用例2代码截图
图4-2-4 测试用例2运行结果
结果分析:Josephring函数中for循环计数代码的结束条件发生错误。若sum值为6则函数初始的pf与ph状态如图4-2-5。因为pf指针一开始是指向的第一个有效节点,ph指向的是头节点。如果count等于2,那么使pf指针和ph指针向前移动一位即可到达要删除节点的位置,所以for循环的结束条件应该为i 图4-2-5 ph与pf初始指向状态图 用例编号:3 期望结果:将所有的节点占用的空间释放掉,并在最后输出“游戏结束”。 运行结果:代码运行正确,并输出了“游戏结束”,如图4-2-6。 结果分析:本函数先将头节点与其后面的节点断开,将head->next置空,让head指针后移一个节点,再利用head与p两个指针依次对后面的节点进行释放空间。 图4-2-6 测试用例3运行结果 用例编号:4 期望结果:正确调用进行游戏所用到的所有函数。 运行结果:运行正确,如图4-2-6。 结果分析:正确将初始船上所有旅客的编号输出,将扔进海里的旅客编号进行了输出,正确将最后剩余的旅客编号顺序输出。 图4-2-6 测试用例4运行结果 通过本次课程设计的题目,我对单向循环链表的创建、遍历以及删除操作有了更加深刻的认识和了解。此次题目采用了单向循环链表的存储形式模拟了约瑟夫生死者游戏的进行过程,我认为单向循环链表与单链表最大的区别就是通过任意一个节点能否将全部的节点遍历一遍。在创建单向循环链表时,只需按照单链表的创建方法(头插法或者尾插法)来创建,在最后一个节点中不将指针域置空,将其存放头节点的地址即可。在对单向循环链表进行操作的时候,只需注意最后的循环结束条件即可。 在此项目的设计过程中,我也遇到了一些问题。一开始,我的思路是在创建头节点时,将头节点的数据域存放编号为1的数据,之后的节点编号值依次递增。但是在输出节点的编号值时,因为题目要求顺序输出剩余的所有节点编号,所以我还是按照从头指针指向的节点开始,依次将后面节点编号输出的算法编写的。在测试的时候当sum=6、count=4时,根据我的算法,我的头节点将被删除,这样我的头指针将脱离整个循环链表而无法正常输出剩余所有节点的编号。起初我的解决思路是:当符合报数条件,要删除pf所指向的节点之前,先判断pf指向的是不是头节点,也就是p==head,如果是,那么将head=head->next即可。但是我编写的代码最终连删除节点的操作都无法执行。考虑到实现比较困难以及后面牵扯到的函数较多,我选择了另一种解决方法:将头节点不存放编号,这样头节点就是一个无效节点,在pf和ph移动的过程中,判断pf是不是指向了头节点,如果是,那么令pf和ph再往前移动一位即可。这样下来,在进行对剩余节点编号顺序输出的时候,从头节点开始,依次往后输出节点编号即可,这个问题也就解决了。 在搜寻文献以及上网查找资料的时候,我还发现了另一种约瑟夫生死者游戏,也就是带密码的约瑟夫生死者游戏。这个游戏将每个人除了进行编号以外,还赋予了每个人不同的密码。游戏规则还是从第一个人开始报数,由这个人的密码确定报数的上限值,将符合报数条件的人扔进海中,下一个人从1开始重新报数,报数的上限值由这个人的密码来确定,直到将一半的人淘汰为止。这种游戏方式使之前提前算好淘汰位置,防止自己被淘汰的人无计可施,增加了游戏的公平性。我的想法是,在结构体中多一个key的区域,在开始进行报数时,将key确定为报数上限值。但由于时间紧迫,没有将算法实现出来。 此次的课程设计给我带来了很大的收获,包括编码能力的提升,搜寻文献查找资料能力的提升以及独立解决问题的能力的提升。最后感谢各位老师的栽培与指导,希望在今后的学习生活中,我可以编写出功能更加丰富、效率更高、问题更加复杂的算法。 [1]耿国华.数据结构:C 语言描述[M].北京:高等教育出版社,2005. [2] 刘汉英.《数据结构》循环链表教学探讨[J]. 科技信息,2009年,35期:151-152. [3]崔进平.约瑟夫问题的几种算法[J].泰安师专学报,2001(6):27-29. [4] 张谞晟.约瑟夫环的C语言实现与应用[J].无线互联科技,2017年,6期:141-142. [5]严蔚敏.数据结构c语言版[M].北京:人民邮电出版社,2015.2.:38-39. [6]谭浩强. C程序设计(第五版)[M].北京:清华大学出版社,2017:314-316.
5 课程设计总结
参考文献
附录 源程序
PNode CreateCLlist(int sum)//创建循环链表
{
int i;
PNode head=NULL;
PNode rear=NULL;
head=(PNode)malloc(sizeof(Node));
if(NULL==head)
{
printf("Memory Failed!\n");
exit(-1);
}
rear=head;
rear->next=NULL;
for(i=1;i<=sum;i++)
{
PNode p=(PNode)malloc(sizeof(Node));
if(NULL==p)
{
printf("Memory Failed!\n");
exit(-1);
}
p->id=i;
p->next=rear->next;
rear->next=p;
rear=p;
}
rear->next=head;
return head;
}
void display(PNode head)//输出船上旅客剩余人数
{
PNode p=head->next;
printf("船上剩余旅客编号:");
while(p!=head)
{
printf("%3d",p->id);
p=p->next;
}
printf("\n\n");
}
void Josephring(PNode head,int sum,int count)//进行约瑟夫环淘汰
{
int i;
int t=1;
int dead=sum/2;//扔进海里的人数
PNode ph=head;//后面的指针 behind
PNode pf=head->next;//前面的指针 front
printf("开始报数:\n\n");
while(dead>0)//扔进海里的人数
{
for(i=1;i