问题导入
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。 [1]
17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。
预期测试效果
按照尾插法插入的 41 个数字编号,经过 39次淘汰后,应该最后剩下来的元素是 16 号和 31 号元素
程序在 VC++2010 上运行的结果如下所示满足预期
循环链表原理图
尾插法原理图
循环链表结构体定义
//数据域
typedef struct ListNode{
//这里的 dateType 为了方便演示就用 int 类型定义
int date;
ListNode *next;
}LinkList,LinkNode;
我们这里解决这个问题的数据结构总共使用到了三个方法
循环链表初始化
void init_List(LinkList *&L){
//初始化循环链表,创建头节点 并分配内存
L = new LinkList;
if(!L){
printf("初始化内存失败\n");
return ;
}
L->date = -1;
L->next = L;
}
插入元素 尾插法
bool ListInsert_back(LinkList*&L,int e){
if(!L) return false;//防御性编程
//找到尾部指指针的前面一个
LinkList* p = L;
while(p->next!=L) p = p->next; //找到尾部元素
//找到之后
LinkList *tmp = new LinkList;
tmp->date = e;
tmp->next = p->next;
p->next = tmp;
return true;
}
遍历双链表元素
void prinLink(LinkList* L){
LinkList *head = L;
L = L->next;
if(!L) return;
while(L!=head&&L){
printf("%d\t",L->date);
L = L->next;
}
}
#实现源代码
#include<stdio.h>
#include<iostream>
typedef struct ListNode{
int date;
ListNode *next;
}LinkList,LinkNode;
//初始化链表
void init_List(LinkList*&L);
//插入元素 尾插法
bool ListInsert_back(LinkList*&L,int e);
//遍历元素
void prinLink(LinkList* L);
#include"List.h"
void init_List(LinkList *&L){
//初始化循环链表,创建头节点 并分配内存
L = new LinkList;
if(!L){
printf("初始化内存失败\n");
return ;
}
L->date = -1;
L->next = L;
}
//插入元素 尾插法
bool ListInsert_back(LinkList*&L,int e){
if(!L) return false;//防御性编程
//找到尾部指指针的前面一个
LinkList* p = L;
while(p->next!=L) p = p->next; //找到尾部元素
//找到之后
LinkList *tmp = new LinkList;
tmp->date = e;
tmp->next = p->next;
p->next = tmp;
return true;
}
void prinLink(LinkList* L){
LinkList *head = L;
L = L->next;
if(!L) return;
while(L!=head&&L){
printf("%d\t",L->date);
L = L->next;
}
}
#include"List.h"
#define PERSON_COUNT 41 //定义游戏总人数
//报数出列
void kill(LinkList *&Heap,int interval = 3){
//从第一个开始报数 每三个出列
int i = 1; //做为淘汰的判断
int j = 1; //记录淘汰的人数
LinkList * L = Heap->next;
printf("\n");
while(PERSON_COUNT-j>=interval-1){ //出局39个元素
//找到要被扔向海中的上一个元素
while(1){
//头节点直接下一个
if(L==Heap) L = L->next; //头节点跳过
if(i>0&&(i+1)%3==0){ //满足条件直接出局
LinkList *tmp = L->next;
if(tmp==Heap){
L = tmp;
tmp = tmp->next;
}
printf("第:%d个出局的编号:%d\n",j,tmp->date);
j++;
i= 1;
L->next = tmp ->next;
L = L->next;
delete tmp;
break;
}else{
L = L->next;
i++;
}
}
}
}
int main(){
LinkList *L = NULL;
init_List(L);
for(int i=1;i<=PERSON_COUNT;i++){
ListInsert_back(L,i);
}
printf("初始化编号\n");
prinLink(L);
kill(L);
printf("最后活下来的人是:\n");
prinLink(L);
printf("\n");
system("pause");
return 0;
}
总结
使用循环链表能通过逐次遍历解决约瑟夫问题,但这种解法并不是最直观的解法,其中头节点是不存储数据元素的,这提高了算法实现的难度.约瑟夫问题通常可以使用递归,数组等方法来解决.