<算法学习>C/C++——约瑟夫问题

目录

题目介绍

问题分析

方法引导

方法一(结构数组)

方法二(链表)

总结


这篇文章是我开始系统学习算法时为记录学习过程写的,比较适合小白,大佬请跳过。不当之处请大家批评指正。

题目介绍

有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!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值