约瑟夫问题
看到了这里,想必大家都已知道约瑟夫问题,这里就不多赘述,只是简单回顾一下问题的描述。
“据说著名犹太历史学家Josephus有过以下的故事: 在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止,然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏.”
目录
一、问题描述
为了方便描述,这里我们假设只有10个人,也就是说10个人围成一个圈,谁数到3谁退出,再由下一个人继续从1开始报数,直到只剩最后一个人。
在题目中,之所以Josephus选择第16和第31个位置,你知道为什么吗?是的,若只剩下Josephus和他朋友的话,谁又能见证他们去自杀呢。
这个问题的解法很多,我们一个个来。
二、约瑟夫问题_递归法
(1)问题分析
很多人认为递归方式很难,实则最简单,因为递归少了很多捣腾,活生生的将复杂的约瑟夫问题转化成了数学问题;
假设共有number个人,循环周期是period,这里number = 10,period = 3,下边进入找规律的时间;
1> 约瑟夫问题的规律:
1.每轮淘汰1个人,那么淘汰number个人就需要进行number轮;
2.每个人上一轮的编号 = (本轮编号 + period)% (本轮总人数);
可能你会问为什么编号要从0开始,因为取余的结果会存在0,可能你又要问为什么非要取余操作,因为这是从图标里归纳出来的。
2> 如何将规律融入代码:
1.共需要进行number轮淘汰;
2.找到第一轮要被淘汰的人的编号;
3.若找到其他轮要被淘汰人的编号,将其转换到第一轮所对应的编号(利用规律2);
(2)代码实现
main.c
#include <stdio.h>
#include "recursion_JosephRing.h"
int main() {
int number, period;
scanf("%d%d",&number,&period);
int list[number];
recursion_JosephRing(number,period,list);
for (int i = 0; i < number; ++i) {
printf("第%d个被淘汰的人是%d\n", i + 1, list[i]);
}
return 0;
}
circlelinkList_JosephRing.h
#ifndef JOSEPHRING_RECURSION_JOSEPHRING_H
#define JOSEPHRING_RECURSION_JOSEPHRING_H
int josephRing(int number,int period,int i){
//number: 第i轮的总人数,period:周期数, i: 第i轮淘汰
if(i==1){
//如果是第1轮,直接输出编号
return (period - 1 )%number;
} else
//否则就进入下一轮寻找编号
return (josephRing(number - 1,period, i-1) + period) % number;
}
void recursion_JosephRing(int number, int period, int *result){
for (int i = 1; i <= number; ++i) {
result[i -1] = josephRing(number,period,i) + 1;
}
}
#endif //JOSEPHRING_RECURSION_JOSEPHRING_H
(3)运行结果
(4)算法解析
1.这个算法也不能说不好,但就代码量来说确实值得推荐,该算法的要点是找到递归规律,即这一轮被淘汰者的编号与上一轮对应的编号之间的关系;
2.T(n) = O(n^2),S(n)= O(n);
3. 这里当然可以构建一个结构体,最终把名字输出出来,会对约瑟夫问题进行更加准确的还原。
(5)BUG记录
1.main函数里的for循环必须从1开始吗?
是的,必须从1开始,否则无法满足约瑟夫问题的数学公式;
2.C语言不能直接返回数组
因为在函数里开辟的空间在函数结束时空间也会随之消失,最好的方法是在参数中引用指针,将数组传递过来修改。
三、约瑟夫问题_循环链表法
(1)问题分析
循环链表直观上最大的特征是可以绕一圈还可以重新回到原来的点上,因此用这个结构刚刚好操作;
1.关于结构体的设置:
typedef struct People{
int number;
struct People *next;
}People;
number的目的是判断这个节点别淘汰了没有;
2.循环链表模拟原理:
逐个遍历每个节点,同时对周期变量进行累加,若一个周期到,且该节点没有被淘汰过,则将该节点淘汰,继续遍历下一个节点,直到所有节点都被淘汰,节点被淘汰的顺序,就是约瑟夫问题中出局的顺序。
(2)代码实现
main.c
#include "recursion_JosephRing.h"
#include "array_JosephRing.h"
#include "circlelinkList_JosephRing.h"
int main() {
int number, period;
scanf("%d%d",&number,&period);
int list[number];
//recursion_JosephRing(number,period,list);
//array_JosephRing(number,period,list);
circlelinkList_JosephRing(number,period,list);
for (int i = 0; i < number; ++i) {
printf("第%d个被淘汰的人是%d\n", i + 1, list[i]);
}
return 0;
}
circlelinkList_JosephRing.h
#ifndef JOSEPHRING_CIRCLELINKLIST_JOSEPHRING_H
#define JOSEPHRING_CIRCLELINKLIST_JOSEPHRING_H
#include <malloc.h>
#include "stdio.h"
typedef struct People{
int number;
struct People *next;
}People;
People *init(int number){ //初始化循环链表
People *linkList = (People*)malloc(sizeof(People)); //开辟节点
People *p = linkList; //做辅助指针
for (int i = 0; i < number; ++i) {
People *node = (People*)malloc(sizeof(People));//建立新节点
node->number = i;
p->next = node;
p = node;
}
p->next = linkList->next;
return linkList;
}
void circlelinkList_JosephRing(int number, int period, int *result){
People *linkList = init(number);
People *p = linkList;
int count = 0; //淘汰人数计数
int j = 0; //周期计数
p = p->next;
while (1){
if(p->number == -1){ //遇到已经被淘汰的人直接跳过
p = p->next;
} else{ //遇到没有被淘汰的人开始计数
if(j == period - 1){
result[count] = (p->number) + 1;
p->number = -1;
count++;
}
p = p->next; //注意该条件
j++;
if(j >= period) j %= period;//重置判断
}
if(count == number) break; //若所有人都已淘汰,则退出循环;
}
}
#endif //JOSEPHRING_CIRCLELINKLIST_JOSEPHRING_H
(3)运行结果
(4)算法解析
1.如果某天要我选择一种方式对约瑟夫问题进行解决时,我的第一选择是递归,第二选择是数组,第三选择是循环链表,因为单独建立一个链表只为解决这个问题的话未免有些大材小用,且不说是否容易实现,但看这个代码量确实不小。
(5)BUG记录
1.关于参数传递问题:
先看这段代码:
void init(People *likList,int number){ //初始化循环链表
linkList = (People*)malloc(sizeof(People)); //开辟节点
People *p = linkList; //做辅助指针
for (int i = 0; i < number; ++i) {
People *node = (People*)malloc(sizeof(People));//建立新节点
node->number = i;
p->next = node;
p = node;
}
p->next = linkList->next;
}
由于编译器总是要为函数的每个参数制作临时副本,所以可以肯定,在调用该函数后,这里的*linkList实际上只是实参的一个副本,该副本在函数中申请了新的内存空间,却也只是把自己的地址给改变了,而外边实参的地址却丝毫不动,所以试图用这种方式将开辟的空间传递出去是不可能实现的,只能用返回地址的方式进行。
那么按照上边这种方式编程会产生什么后果呢?事实上,因为每一次执行init函数都会申请一块内存,但申请的内存却不能有效释放,结果是内存一直被独占,最终造成内存泄露,程序出现崩溃。
简单来说,该bug就是:init函数不能传递动态内存。
解决办法如下:
People *init(int number){ //初始化循环链表
People *linkList = (People*)malloc(sizeof(People)); //开辟节点
People *p = linkList; //做辅助指针
for (int i = 0; i < number; ++i) {
People *node = (People*)malloc(sizeof(People));//建立新节点
node->number = i;
p->next = node;
p = node;
}
p->next = linkList->next;
return linkList;
}
直接将新的地址返回,可以有效解决这一个问题。
2.另外需要注意:
1.malloc函数返回的是新空间的首地址;
2.当数组作为参数传入函数中,在函数内可以修改数组的原因是,实参和形参指向的是同一片内存区域,而我们修改的是内存区域中的内容;
3.C语言里的&只是取址符,C++里的&不仅是取址符,还有引用的意思;
int iv = 12;
int &riv = iv; //声明了一个变量riv,同时初始化了,也就是riv是iv的别名。
4.关于p指针的移动,p = p->next放置的位置需要谨慎。
四、约瑟夫问题_数组法
(1)问题分析
约瑟夫问题用数组解决就是一遍遍刷新数组的问题,数组起始全是0,在第一遍刷新的时候将遇到的周期位置替换为-1,第二遍继续替换,直到整个数组全是-1,数组中数字被替换的顺序就是约瑟夫问题中被淘汰者出局的顺序;
(2)代码实现
main.c
#include "recursion_JosephRing.h"
#include "array_JosephRing.h"
int main() {
int number, period;
scanf("%d%d",&number,&period);
int list[number];
//recursion_JosephRing(number,period,list);
array_JosephRing(number,period,list);
for (int i = 0; i < number; ++i) {
printf("第%d个被淘汰的人是%d\n", i + 1, list[i]);
}
return 0;
}
array_JosephRing.h
#ifndef JOSEPHRING_ARRAY_JOSEPHRING_H
#define JOSEPHRING_ARRAY_JOSEPHRING_H
#include <stdio.h>
void array_JosephRing(int number,int period, int *result){
int count = 0; //已淘汰人数计数
int pointer = 0; //记录下一轮淘汰的下标值
int list[number];
for (int i = 0; i < number; ++i) { //数组的初始化
list[i] = 0;
}
int j = 0;
while (1){
if(list[pointer] == -1){ //遇到已经被淘汰的人直接跳过
pointer++;
} else{ //遇到没有被淘汰的人开始计数
if(j == period - 1){
result[count] = pointer + 1;
list[pointer] = -1;
count++;
}
j++;
if(j >= period) j %= period;//重置判断
pointer++;
if(pointer >= number) pointer %= number;
}
if(count == number) break; //若所有人都已淘汰,则退出循环;
}
}
#endif //JOSEPHRING_ARRAY_JOSEPHRING_H
(3)运行结果
(4)算法解析
数组法解决这个问题的思路与链表法一样,但是比链表法更加简洁,如果说递归难以理解的话,那么这种解法是约瑟夫问题解决方案的最佳选择。
(5)BUG记录
1.关于variable-sized object may not be initialized 的报错;
这里有一条正常的初始化语句(语句1):
int list[10] = {0};
还有一条会报错的初始化语句(语句2):
int variable = 10;
int list[variable] = {0};
语句2之所以会报错是因为C语言规定了不能使用variable-sized的数组,更深层次的原因:数组是连续的内存,需要在程序启动前就分配完毕,如果动态创建的话,那么就没有办法保证和检测到如此多的连续内存空间。
2.关于是否重置序号问题;
if(j == period) j %= period;
如果j超过period时就要进行取余重置,上边这条写法是不安全的,一旦j在判断前能够进行两次自增,那么就会直接跳过这个判断条件,比如:若period是10,此时j是9,若j自增两次变为11,则period这个条件已经不能对其进行判断;
所以应该这么写:
if(j >= period) j %= period;
本文只是提供一个大体的思路和实现办法,并没有过于具体的展开讲解,如有问题欢迎留言,我们共同学习。