一個經典約瑟夫問題的分析與解答
一、約瑟夫問題的由來
約瑟夫問題(Josephus)是由古羅馬的史學家約瑟夫(全名Titus Flavius Josephus)提出的。它是一個出現在計算機科學和數學中的經典問題。在計算機編程的算法中,類似問題又稱爲約瑟夫環。(更好的閱讀體驗,移步程序員在旅途)
Josephus是1世紀的一名猶太曆史學家。他在自己的日記中寫道,他和他的40個戰友被羅馬軍隊包圍在洞中。他們討論是自殺還是被俘,最終決定自殺,並以抽籤的方式決定誰殺掉誰。41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡爲止。然而Josephus 和他的朋友並不想遵從。首先從一個人開始,越過k-2個人(因爲第一個人已經被越過),並殺掉第k個人。接着,再越過k-1個人,並殺掉第k個人。這個過程沿着圓圈一直進行,直到最終只剩下一個人留下,這個人就可以繼續活着。Josephus將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。
約瑟夫問題的一般描述形式爲:設有編號爲1,2,……,n的N個人圍成一個圈,從第1個人開始報數,報到M時停止報數,報M的人出圈,再從他的下一個人起重新報數,報到M時停止報數,報M的出圈,……,按照這個規則進行下來,直到所有人全部出圈爲止。當任意給定N和M後,構建相關數據結構與算法求N個人出圈的次序。
二、典型例題
有n個人圍成一圈,順序排號。從第一個人開始報數(從1到3報數),凡報到3的人退出圈子,問最後留下來的是原來第幾號的那個人。
三、分析解答
可以採用數組或者循環鏈表的數據結構來解答這道題目。各有優點。數組實現起來簡單,但是邏輯上覆雜。循環鏈接邏輯上簡單,實現起來複雜。下面採用兩種方法解答此題。
3.1 採用數組存儲數據
利用數組保存這N個人的序號,設計兩個計數器,一個k作爲報數計數器,一個m作爲退出人數報數器。從第一個人開始計數(數組下標i=0),計數器k到3後清零,數組元素報到最後的時候再從第一個人開始。每退出一個人,相應的數組元素置0,報數計數時只對非0元素計數,當計數器m到n-1是說明只剩下一個人,這時候算法結束,輸出剩下人的編號即可。
#include<stdio.h>
#define NMAX 50
int main(){
//k 報數計數器
//m 退出人數計數器
int i,k,m,n,num[NMAX];
scanf("%d",&n);
//給N個人寫上序號
for(i=0;i<n;i++){
num[i] = i+1;
}
i = k = m =0;
// m = n-1 時說明只剩下一個人
while(m < n -1){
//當前位置的值不爲0,則計數,爲0代表退出了
if(num[i] != 0){
k++;
}
//計數器K=3的位置人數退出,退出的位置記爲0。計數器歸0重新計數。退出人數m+1;
if(k == 3){
num[i] = 0;
m++;
k = 0;
}
i++;
if(i == n){
i = 0;
}
}
//輸出最後留下來的那個數
i= 0;
while(num[i] ==0){
i++;
}
printf("Left is %d\n", num[i]);
return 0;
}
3.2 採用循環鏈表存儲數據
利用循環鏈表存儲N個人的序號。將報到k=3的節點從循環鏈表彙總移除,最後只剩下一個節點循環結束,輸出剩下人的編號即可。(其中報數器K的值可以自定義)
#include<stdio.h>
#include<stdlib.h>
typedef struct Num_node{
int data;
struct Num_node *next;
}Num_Node;
int main(){
//n 總人數,m退出的人數,k報數計數器
int n,k;
Num_Node *head = NULL , *tail =NULL, *p = NULL;
int i=0;
//輸入N,然後初始化鏈表
scanf("%d",&n);
while(i++<n){
//申請節點所佔用的內存空間
if( (p = (Num_Node *)malloc(sizeof(Num_Node))) == NULL){
printf("memery is not available");
exit(1);
}
p->data = i;
p->next = NULL;
//當前申請的是第一個節點
if(head == NULL){
head = tail = p;
}else{
//鏈尾插值
tail->next = p;
tail = p;
}
//如果是最後一個元素,讓其指向head:這樣就構成了循環鏈表
if(i == n){
tail->next = head;
}
}
//凡是報到k=3的節點就從鏈表中去除。(這個k可以自己定義,只要K<N就行)
k = 3;
p = head;
Num_Node *pre_p = NULL; //p的前驅指針,刪除p的時候,需要用到這個指針。
while(p->next != p) //直到p指向自己,說明只剩下一個元素了。
{
for(i = 1; i < k; i++)
{
pre_p = p;
p = p->next;
}
pre_p->next = p->next;
free(p); // 刪除結點,從內存釋放該結點佔用的內存空間
p = pre_p->next;
}
printf("Left is %d \n",p->data);
return 0;
}
三、總結
約瑟夫問題是一個經典的計算機與數學問題,由來已久,解法也各異。上面兩種方法,分別利用了數組、循環鏈表數據結構來分析這個題目,雖然都可以解決,但是邏輯上的複雜性卻有很大的差異。這其中就能夠看出數據結構在解決問題過程中的重要性,合適的數據結構會大大降近解題的難度。