约瑟夫问题

------<a href="http://www.itheima.com" target="blank">Java培训、Android培训、iOS培训、.Net培训</a>、期待与您交流! -------


约瑟夫问题
今天,公司有个同事问我一个问题,是关于约瑟夫问题的,然后我就跟他讲解了半天,直到他弄懂了为止。其实,在这个过程中,不仅他从中学到了不少收获,而且我从中也得到了不少锻炼,我觉得这道题还是蛮不容易懂的,而且可以从这个问题中学到很多知识,在很多面试中也会问到,如果能把这道题写出来,对我们的工资又多了一点筹码啊,在这里我就班门弄斧了,这仅仅只是我个人的见解,如果有技术大牛不同意我的看法,可以提出来。
咱们就开始正题了啊,问题是这样的:耶稣有13个门徒,其中有一个就是出卖耶稣的叛徒,请用排除法找出这位叛徒:13人围坐一圈,从第一个开始报号:1,2,3,1,2,3...。凡是报到“3”就退出圈子,最后留在圈子内的人就是出卖耶稣的叛徒。请找出它原来的序号。
问题说到这里,有很多人就会提问或者是疑问啊(大致都提了这些问题):
1、一轮(这里代表从第一个人开始报数,直到最后一个人,也就是我们这里的第十三个人)报数完了之后,又怎么从第一个人重新开始报数呢?
2、怎么去表示某个人已经被踢出去了呢?
3、报数为3的这个人报数完了之后,就会被踢出去,那怎么样让紧接着他后面的人开始从1开始重新开始报数呢?
4、怎么确定参与循环的人数呢?
如果大家把这几个问题解决之后,那么这道题的答案就已经出来了。
那么我们就来捋一捋思路啊。
第一步:从题目中我们可以获取几个变量,一个是耶稣门徒的人数,
我们定义为people_num,报数的号码,定义为step,还有一个就是
我们是通过什么,每次报数的人都是下一个人,这里我们就要通过
循环,遍历每个人,所以这里我们要通过数组的下标,取出每个人
的位置,所以就还得需要一个下标变量,我们定义为pos (position位置)
第二步:我们得把每个人的位置存着,所以我们的定义一个数组,名为people
第三:每次报数完了之后,就会有个人出去,所以每次都得减少一个人,
直到数组里面没有人之后,就不再报数这时我们就可以确定循环条件。
第四步:我们之前所说的把报数为3的人踢出去,其实是个假象,
我们并没有把它踢出去,而是将它的值修改成0,如果把这个人踢出去,
就改变数组的结构,我们就不知道它原来的位置了。所以我们这里判断
此时报数的人编号不为0,我们就可以报数。         
第五步:还是判断,如果报数的这个人编号不为0,而且报的号是3,
那么我们就让这个人“踢出去”,将报数为3的这个人的编号改为0,
那么下次循环的时候,就会跳过他。  
第六步:还是判读,如果报数的这个人的编号为13,那么的下一个
报数的人得从第一个人开始,所以我们得把下标重置为0
伪代码:


//如果报数的人大于0,就说明数组里面还有人,就还得循环
while(报数的人数 大于 0)
{
//如果报数人的编号不为0,那么他的报数就应该加1
if(报数人的编号 不为 0)
{
报的数字加1,也就是step++
}


if(报的数为3 并且 报数人的编号不为0)
{
那么这个人就可以被踢出去了,并把他的位置打印出来
并且把这个被踢出去的人的编号改为0
此时,参与报数的人就会少一个
}
//下一个人报数,所以的遍历数组
下标加1


if(如果这个报数的人的位置超过数组的长度,就表示已经越界,我们就得从第一个人开始报数)
{
下标重置为0
}


if(如果报的号是3就踢出去,我们又得从1开始报数,所以我们得把报数重置){
报数重置为0
}
}


第一种方法:(推荐使用)
 
//实际代码的实现:
#import <stdio.h>

#define ALL_PEOPLE_NUM 13
#define STEP 3

int main(int argc, const char * argv[])
{
	int people_num = ALL_PEOPLE_NUM;
	int step = 0;
	int pos = 0;
	int people[ALL_PEOPLE_NUM];

	for(;pos < people_num; pos++)
	{
		people[pos] = pos + 1;
	}
	//此时的pos为13
	pos = 0;//因为我们得从第一个人开始报数

	while(people_num > 0)
	{
		if(people[pos])
		{
			step++;
		}
		if(step == STEP && people[pos])
		{
			printf("%d\n",people[pos]);
			people[pos] = 0;
			people_num--;
		}
		pos++;
		if(pos == ALL_PEOPLE_NUM)
		{
			pos = 0;
		}
		if(step == STEP)
		{
			step = 0;
		}
	}
	return 0;
}


第二种方法:使用for循环的嵌套,其实,思路都是一样的

 
#import <stdio.h>
#define ALL_PEOPLE_NUM 13
#define<span style="white-space:pre">	</span>STEP 3
int main(int argc, const char * argv[])
{
	
	int people[ALL_PEOPLE_NUM] = {0};//这里的0就是每个人的编号
	int step = 0;
	int pos;//这个代表每个人的位置
	for(int j = 0; j < ALL_PEOPLE_NUM; j++)
	{
		//这句话就会从第一个人开始报数,直到最后一个人,但是没有取完
		//比如13个人,就只取出了3、6、9、12,所以如果要找出最后一个人,就得在最外面再加一层for循环
		for(pos = 0; pos < ALL_PEOPLE_NUM;pos++)
		{
			//如果报数人的编号为0,就参与报数
			if(people[pos] == 0)
			{
				step++;
				//如果代码能执行到这里,说明这个报数人的编号为0
				//并且如果报数人报的数为3,就将他“踢出去”,变为非0的值
				if(step == STEP)
				{
					people[pos] = -1;
					step = 0;
					printf("%d\n",pos+1);
				}
			}
		}
	}

	return 0;
}

注意:使用嵌套for循环,效率很低,因为不管数组里面还有没有人,它都会去循环,时间复杂度会提高,消耗更多的内存资源,所以不建议使用。


第三种方法:使用递归(既然给大家讲得详细一点,我就去查了一些资料)
假设下标从0开始,0,1,2 .. m-1共m个人,从1开始报数,报到k则此人从环出退出,问最后剩下的一个人的编号是多少?


现在假设m = 10 k = 3,那么它的下标就是


0  1  2  3  4  5  6  7  8  9


第一个人踢出去之后的序列为:


0  1  3  4  5  6  7  8  9


接下来,就是第三个人开始报数 即:


3  4  5  6  7  8  9  0  1 (*)


我们把该式转化为:


0  1  2  3  4  5  6  7  8  (**)


则你会发现:((**)+3) % 10则转化为(*)式了


也就是说,我们求出9个人中第9次出环的编号,最后进行上面的转换就能得到10个人第10次出环的编号了


设f(m,k,i)为m个人的环,报数为k,第i个人出环的编号,则f(10,3,10)是我们要得结果


当i = 1时,f(m,k,i) = (m+k-1)%m


当i!= 1时,f(m,k,i) = (f(m-1,k,i-1)+k)%m


所以代码如下:


#import <stdio.h>

int fun(int m,int k,int i);

int main(int argc, const char * argv[])
{
	for(int i = 1;i < 13; i++)
		printf("第%d次出环:%d\n",i,fun(13,3,i)+1);
	return 0;
}

int fun(int m,int k,int i)
{
	if(i == 1)
		return (m+k-1)%m;
	else
		return (fun(m-1,k,i-1)+k)%m;
}

}


第四种,通过数学方法  //约瑟夫环问题数学解法,时间复杂度为O(n)

代码如下:

#import <stdio.h>
#define N 13
#define M  3

int main(int argc, const char * argv[])
{
    int i, s = 0;
    
    for (i = 2; i <= N; ++i)
        s = (s + M) % i;
    
    printf("叛徒 is %d\n", s + 1);
    return 0;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值