經典演算法--約瑟夫環問題的三種解法
約瑟夫環問題,這是一個很經典演算法,處理的關鍵是:偽連結串列
問題描述:N個人圍成一圈,從第一個人開始報數,報到m的人出圈,剩下的人繼續從1開始報數,報到m的人出圈;如此往復,直到所有人出圈。(模擬此過程,輸出出圈的人的序號)
在資料結構與演算法書上,這個是用連結串列解決的。我感覺連結串列使用起來很麻煩,並且這個用連結串列處理起來也不是最佳的。
我畫了一個圖用來理解:
有如下問題需要首先考慮:
1、“圈”怎樣形成?
以上圖為例:下標從0開始,當下標為11的時候,在加1,就應該回到0。
index = (index+1) % count;
2、怎樣處理?陣列or連結串列or其他方法?
連結串列使用起來很笨重,我們有迴圈陣列的方法。
解法一程式分析:
迴圈的開始和結束:迴圈的結束取決於圈內是否還有“人”,可以用一個變數alive表示初始人數,每一次出圈,alive-1.判斷alive是否非0即可。
while(alive > 0)
每一次迴圈,就是“過”一個人,但是,這個人有兩種不同的狀態:在圈內和不在圈內;在圈內就報數,number+1,不在圈內就不參與報數,number不變。
假設有N個int元素的陣列,每一個int元素表示一個“人”;並且,取值為0和1, 1表示在圈內,0表示不在圈內,所以,如果這個人在圈內,number+1;如果這個人不在圈內,number+0。那麼,在報數的時候,不需要考慮這個人在不在圈內就行(每一個人都需要加1或加0,所以,可以在這塊優化一下程式)。
void joseph(int count, int doom) { int alive = count; //倖存人數 int number = 0; //計數,當number==doom時,淘汰這個人 int index = 0; //下標,為總人數-1 int *circle = NULL; //根據需求設為迴圈陣列,儲存每個人 //用calloc()函式申請得到的空間,自動初始化每個元素為0 //所以,0表示在這個人在約瑟夫環內,1表示這個人出圈,即“淘汰” circle = (int *) calloc(sizeof(int), count); //只要倖存人數大於0,則一直進行迴圈 while(alive > 0) { number += 1- circle[index]; //每輪到一個人報數,不管是"0"還是"1"都進行計數 if(number == doom) { //當number==doom時,就要淘汰當前這個人 /* 淘汰一個人需要做四步操作: 1、輸出這個人的位置 2、把這個人的狀態從在圈內"0"改為不在圈內"1" 3、倖存人數alive-- 4、 計數器number歸零 */ alive == 1 ? printf("%d", index+1) : printf("%d,", index+1); circle[index] = 1; alive--; number = 0; } //與總人數count取餘,則可以使index在0~count-1之間 一直迴圈,達到迴圈陣列的目的 index = (index +1) % count; } printf("\n"); free(circle); //結束後一定要釋放circle所申請的空間 }
解法二程式分析:
解法二在解法一的基礎上進行了優化,對出圈的人的節點進行刪除,可以減少時間複雜度。
假設,要刪除的節點下標為curIndex,其前驅節點下標為preIndex;
circle[preIndex] = circle[curIndex];
假設要刪除的節點下標curIndex=3,那麼circle[2] = circle[3] ,circle[2] = 4,circle[2]之前等於3,現在等於4。即下標為3的第四個人已經被刪除了。
怎樣遍歷?
preIndex = curIndex;
curIndex = circle[curIndex];
但是,在出圈的時候,curIndex 和preIndex 的變化有別於上面的操作:
出圈時,curIndex 需要後移,preIndex 應該不動!
每次迴圈,直接報數,因為被刪除的人(例如上面的第四個人)不可能進行報數了。
void joseph(int count, int doom) {
int alive = count; // 倖存人數
int number = 0; // 報數的數
int curIndex = 0; // 當前人下標
int preIndex = count - 1; // 前一個人下標
int *circle = NULL;
int index;
circle = (int *) malloc (sizeof(int) * count);
//對circle陣列進行初始化
for(index = 0; index < count; index++) {
circle[index] = (index + 1) % count;
}
while(alive > 0) {
number++;
if(number == doom) {
alive == 1 ? printf("%d", curIndex+1) : printf("%d,", curIndex+1);
alive--;
number = 0;
circle[preIndex] = circle[curIndex]; //出圈操作
} else {
preIndex = curIndex; //處理下一個人
}
curIndex = circle[curIndex];
}
free(circle);
}
解法三程式分析:
解法三裡沒有進行number報數,而是直接計算出需要移動的人數,然後定位到要出圈的人。
num = doom % alive - 1;
for(index = 0; index < (num == -1 ? alive - 1 : num); index++) {
preIndex = curIndex;
curIndex = circle[curIndex];
}
void joseph(int count, int doom) {
int alive = count; // 倖存人數
int curIndex = 0; // 當前人下標
int preIndex = count - 1; // 前一個人下標
int *circle = NULL;
int index;
circle = (int *) malloc(sizeof(int) * count);
for(index = 0; index < count; index++) {
circle[index] = (index + 1) % count; // 初始化連結串列
}
while(alive > 0) { // 只要還有幸存者,就繼續“殺”
int num = doom % alive - 1; // 直接計算出需要移動的人數,
// 直接定位到要出圈的人
for(index = 0; index < (num == -1 ? alive - 1 : num); index++) {
preIndex = curIndex;
curIndex = circle[curIndex];
}
// 該人出圈!
alive == 1 ? printf("%d", curIndex+1) : printf("%d,", curIndex+1);
alive--;
circle[preIndex] = circle[curIndex]; // 真正的出圈操作!
curIndex = circle[curIndex]; // 繼續處理下一個人
}
// 這個演算法比normalJoseph.c效率提高30%!
free(circle);
}
GitHub原始碼地址:
https://github.com/yangchaoy259189888/JosephRing/