【循环链表】约瑟夫问题

原型

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。问题是,给定了数,一开始要站在什么地方才能避免被处决?
Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。(摘自百度百科)

问题描述

:“被杀掉”看起来有些过于血腥,下面用“击鼓传花”代替。
n个人围成一圈,从第s个开始报数,每隔m个人可以拿到花,最后剩下一个人没有花花。
e.g n=6,s=1,m=5,拿到花的序号依次是5,4,6,2,3,最后剩下1号。(可自行画图理解)

变式:m可以为变量,比如拿到花的人的编号

解题思路

本题实际上是一个不断查找某一特定位置的元素的问题,由于要找到下一个人,依赖于上一个人的位置以及前几轮已被挑出来的人的位置,因此没有可以直接套用的数学公式。
在该问题中,最重要也是最难的问题就是如何判断某一位上的元素是否已经被选出来过。对此,若用数组,解决方法为对选出来的元素做一个标记(flag),缺点是这些元素仍然在数组中,遍历次数相当可观;若用链表,解决方法为建立循环链表,对选出来的元素(结点)直接执行删除操作,则可以最大程度节省时间。

解决方案(源代码)

咱们先设定一套丢花花的规则

int number,start,stop;//人数,开始的编号,停下时经过的人数
printf("please enter number,start and stop");
scanf("%d %d %d",&number,&start,&stop);
//这里为了便于理解用的单词,可简化为字母
if(start>number)
return -1; //提高程序健壮性

程序主要分为两个部分,链表的建立与元素的查询及删除。

1、建立循环链表

结点的初始化定义

typedef struct NODE //将struct重命名为NODE
{
int data; //结点的数据域(也可以是其他任何类型,甚至可以是结构体哟)
struct NODE *next; //结点的指针域
}LNode,*Linklist; //定义了一个结点(结构体)以及一个指向结点(结构体)的指针

这里之所以没有像教材一样把NODE和LNode用同一个词表示,是因为其实在后面要给新增结点申请空间的时候,用的都是LNode的大小,而与NODE无关,故避免混淆。

循环链表的建立

Linklist head=NULL; //定义一个指向第一个结点的指针
Linklist s,r=NULL; //定义两个指针,其中s用于建立新结点,r用于定位及查找结点
for(int i=1;i<=number;i++)
{
	s=(LNode *)malloc(sizeof(LNode)); //申请一个大小和前面定义结构体相同的结点,并用强制类型转换,转换为指针类型
	s->data=i;	//装填数据
	if(head==NULL)
		head=s;	//使头指针指向第一个结点
	else
		r->next=s;	//通过r指针的操作使每个结点的指针域为下一个结点的地址
	r=s; //使r向后移动指向最后一个结点
} 
r->next=head; //将最后一个结点与第一个结点连接上,形成循环
r=head;	//这句使指针回到第一个结点,可有可无

2、元素的查询及删除

接下来的步骤就是找到开始的人-依次计数-拿到花的人离开-从下一个人开始重复上述过程。
然而在实际操作中,我们会发现在单向循环链表中,删除一个元素的后继元素比删除该元素本身要方便得多(后者需要从头开始遍历找到该元素的前驱),因此可以考虑找到离开的人的前一个人时就停下。当然,需要解决stop=1,就是从开始的位置依次离开的情况,可以直接用遍历的方式依次输出结点内容。

void delete_nextelem(Linklist r)
{
	Linklist f;	//建立一个辅助指针
	f=r->next;
	printf("被删除的元素是:%d\n",f->data);
	r->next=f->next;
	free(f); //释放被删除结点的内存空间
} /*删除函数定义*/
int count=1;
while(count!=start)
{
	r=r->next;
	count++;
} /*找到开始的位置*/
if(stop==1)
	for(i=0;i<number-1;i++)
	{
		printf("被删除的元素是:%d\n",r->data);
		r=r->next;
	} 
else
	for(i=0;i<number-1;i++)
	{
		count=1; //从第一个元素开始计数
		while(count!=(stop-1)%number) //用求余的方式可以减少循环次数
		{
			r=r->next;
			count++;
		} //计数到被删除结点的前一位
		delete_nextelem(r); //调用删除结点的函数
		r=r->next; //从下一位开始重复上述过程
	}	/*条件语句遍历输出各个结点的元素*/
printf("最后一个元素是:%d\n",r->data);

至此,整个问题就解决了。在这个题目中,最关键的是循环链表的建立(让末尾结点指针域指向头结点),以及在删除时用一点小技巧降低时间复杂度(但要注意特殊情况)。
另外,在实际操作过程中,我还发现很容易搞错循环终止的条件以及变量的初始值,尤其是start和stop都是从1开始计数,所以对应的count也应该以1作为初始值。

3、考虑变式,stop为被删除的人的编号(data)

一开始我以为只需要改条件语句,即每一次进入循环时都要重置stop的值。但实际操作中需要单独考虑删除第一个结点的情况。因为释放了第一个结点的空间后,stop=1,但在r已经移动到要删除的结点时,delete_nextelem函数无法进行操作(删的是下一个结点)。这个时候只要用一个条件语句,在遇到第一个结点时,不移动r指针的位置即可。

        for(i=0;i<number-1;i++)
        {
        if(stop==1)
        {
            stop=r->next->data; //重置stop的值
            delete_nextelem(r);//调用删除结点的函数
            r=r->next;
        }
        else
        {
            count=1; //从第一个元素开始计数
            while(count!=(stop-1)%number) //用求余的方式可以减少循环次数
            {
                r=r->next;
                count++;
            } //计数到被删除结点的前一位
            stop=r->next->data; //确定下一次移动的次数
            if(stop==1)
            	delete_nextelem(r); //不用移动r
            else
            {
            	delete_nextelem(r); //调用删除结点的函数
                r=r->next;
            } //从下一位开始重复上述过程
        }
        }    /*条件语句遍历输出各个结点的元素*/
printf("最后一个元素是:%d\n",r->data);

总结

虽然这道题的思路很简单,循环链表也不太会像数组一样出现判断尾部的问题,但是在实践的时候还是有各种各样的bug。还是要多设断点调试。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值