目录
这篇文章是我开始系统学习算法时为记录学习过程写的,比较适合小白,大佬请跳过。不当之处请大家批评指正。
题目介绍
有n个小孩围成一圈并依次编号,教师指定从第m个小孩开始报数,当报到第S个小孩时,即令其出列,然后再从下一个孩子起从1开始继续报数,数到第s个小孩又令其出列,这样直到所有的孩子都依次出列。求小孩出列的顺序。
问题分析
这个问题的关键在于理解题中所给三个数字的意义。重述一下,n代表小孩数,m代表从哪个小孩开始,s代表数到几出局。
我们很容易就能想到,要把这n个小孩顺序存起来,每次报到s时删除那个小孩,再从下一个人开始报数,如此循环下去。
方法引导
方法一(结构数组)
根据上面的分析,我们很容易想到用数组来存储孩子们的编号。
但是我们面临的问题是,如何把这些孩子连成一个环?如何“删除”某个孩子(需要把其后所有人往前移动吗?)等等。
其实我们要实现顺序连接孩子,就不能只关注孩子们的编号,也要关注他们后面的孩子,这就提示我们孩子的信息>=2,所以为了方便算法实现,我们可以利用结构数组来存孩子的信息,如下所示。
struct child
{
int id; //孩子的序号
int next; //下一个孩子的序号
}link[200]; //容量为200个孩子的结构数组
有了这个结构数组之后,我们就可以把孩子连接成环了。具体操作如下。
for(i=1;i<=n;i++) //假设有n个孩子
{
link[i].id=i; //给孩子的序号赋值
if(i==n)
{
link[i].next=1; //如果当前遍历到的是最后一个孩子,则他的下一个人是第一个孩子
}
else
{
link[i].next=i+1; //否则他的下一个人就是数组中下一个孩子
}
}
准备工作做好了,下面我们就可以开始游戏了。首先我们应该找到开始计数的孩子的前一个孩子(原因后面会讲)。方法如下。
int k; //假设k是开始报数小孩的前一个小孩
if(m==1) k=n; //当开始报数的小孩序号为1时,k为n(最后一个小孩)
else k=m-1; //否则,即为开始报数小孩的前一个小孩
下面我们需要循环n次,每次让数到s的孩子出局。方法如下。
for(t=0;t<n;t++) //t为循环次数
{
for(i=1;i<s;i++) //循环s-1次,让k往后遍历到出局的小孩那里
{
k=link[k].next; //这里解释了刚才为什么要找开始小孩的前一个小孩。
//因为每次遍历到的当前小孩都是取上一个小孩的下一个序号
if(link[k].id!=0) i++; //如果当前小孩的序号为0,说明已经出局了,不用再参与循环
}
//遍历完s-1个小孩时,k来到了报s的小孩这里
printf("%d\n",link[k].no); //输出出局小孩的序号
link[k].no=0; //把出局小孩的序号置0,以后不再参与遍历(呼应上面for循环的第二条语句)
}
总结一下:结构数组的方法其实是完全贴合了我们最初的设想来完成游戏的,我们先把小孩连接成一个环,找到开始的小孩(由于后续代码逻辑需要,我们找了开始小孩的前一个小孩),然后循环n次,每次循环时都报s个数,不停往后更新k(当前小孩),并输出第s个小孩的编号,然后让第s个小孩出局(我们用id=0的形式表示其出局)。
方法二(链表)
其实由上面的分析,链表的直觉已经蠢蠢欲动了!毕竟在结构里设next是链表的特色嘛!
开始分析之前,希望大家不要对链表有畏难情绪,虽然我本人当初学C的时候也不喜欢指针,但是我真心觉得链表太好用了!!!坚持一下理解它,我们就拥有了一个强大的帮手帮我们解决各种问题!!
我们考虑一下方法一,用数组储存所有孩子的所有信息,是不是很麻烦?而且我们需要预设孩子的数量,假设我们不知道孩子数呢?假设我们开了200个孩子数组空间,结果只有10个孩子,会不会很耗空间?最重要的是,数组的存储空间是连续的!如果使用数组存储,我们需要在内存中找到一块满足要求的连续空间。
没错!链表就是来给我们提供动态存储空间的帮手!试想一下,输入一个孩子的信息前开辟一块空间用来存储,并且不要求存储空间连在一起,它像补丁一样利用着存储空间,却也能让各个空间有所联系!
于是我们开始尝试用链表解决约瑟夫问题。首先还是建立存储孩子信息的结构体。
typedef struct node
{
int id; //孩子的序号
struct node *next; //链接下一个孩子的结构体指针
}NODE;
此处我们用了typedef定义数据类型,以后定义该结构体变量就不用输struct node了,直接输NODE即可。
然后我们可以构建环形链表。方法如下。
NODE *head,*p,*q;
head=(NODE*)malloc(sizeof(NODE));//建立环形链表
head->id=-1; //头指针不在孩子的范围内,把它的序号设成-1
head->next=head; //让head链接下一个孩子的指针直接指向head的头
依次插入每个孩子。过程如下。
for(int i=n;i>=1;i--) //n为孩子个数
{
p=(NODE*)malloc(sizeof(NODE)); //开辟空间
p->next=head->next; //让head链接的下一个孩子指向p链接的下一个孩子
head->next=p; //再让head链接p,就相当于在head和head后面一个孩子中间插入了p这个孩子
p->id=i; //for循环中倒着往前循环,因为每次都插在head后,序号正好是i
}
如果没明白为什么i从n开始倒着往前循环,可以看看下面的图解。
让最后一个孩子连接第一个孩子。方法如下。
while(p->next!=head) //如果不是最后一个人就一直往后遍历
{
p=p->next;
}
p->next=head->next; //找到最后一个人,让它和第一个人连成环
如果没理解就看一下下面的图解。
好了,准备工作完成了,开始游戏。
首先找到第一个孩子
while(p->id!=m)//找到开始的人
{
p=p->next;
}
然后就可以开始报数了。
for(i=0;i<n;i++)
{
for(j=1;j<s-1;j++)
{
p=p->next; //p为要出局的人的前一个人
}
q=p->next; //q为要出局的人
p->next=q->next; //p后一个人出局
printf("%d\n",q->id); //输出出局小孩的序号
free(q); //释放出局的人
p=p->next; //p移到下一个人
}
解释一下为什么要让p遍历到出局的人的前一个人。因为如果正好遍历到出局的人,那当然可以直接输出他的序号,释放他,但是这样就没办法找到他前面的那个人,让他前面的那个人连上他后面的那个人了(我们建立的是单向链表)。所以我们就让p遍历到出局的人的前一个人,然后让q表示p后面的人,也就是出局的人,这样运用链表中next指针的功能,p还可以链接到出局人后面的那个人。
总结一下:我们可以清晰的看到链表的好处,就是“删除”小孩时是真的“删除”了,不需要用id=0的方式表示,也就意味着每次遍历时不用先判断当前小孩的id是不是0,需不需要跳过了。
扩展--双向链表法:其实还有一种方法,可以用双向链表,这样就不用担心是遍历到出局的人还是它前面那个人了。而且这个方法更加省时。假设我们有20个小孩,数到15的人出局。我们完全可以从正向遍历15个小孩改为反向遍历6个小孩。具体情况下可以选择遍历次数更少的方式。有兴趣的同志们可以搜一下双向链表研究一下。
总结
以上我们介绍了两种方法来解决约瑟夫问题,分别是结构数组法和链表法。我们的思路都是先构造一个环,存储小孩的信息。然后找到第一个小孩,遍历n次,每次都报s个数,输出第s个小孩的序号,然后让第s个小孩出局。
输出时我直接用了回车,有些题可能对输出要求比较复杂,比如十个一行,空格隔开之类的,大家自行调整一下即可,也比较简单,在此不再赘述。
要点过程代码已经放在上面了,伪代码也叙述的差不多了,大家根据自己的题目加入输入输出就可以了。由于很多题的细节要求不一样,所以完整代码就没必要贴了。对于新手来讲,我更推荐大家按照伪代码写自己的代码,遇到写不下去的地方再参考网上的代码,这样才能真正提高代码能力。一开始可能会有点难,但是坚持一下你会很有成就感的!(毕竟Ctrl c v会上瘾,不要以后才追悔莫及哦)
希望能对大家有所帮助,也是记录一下我学习的过程,以后方便复习。祝愿大家多多AC!