1. 程式人生 > >通過例子進階學習C++(六)你真的能寫出約瑟夫環麼

通過例子進階學習C++(六)你真的能寫出約瑟夫環麼

本文是通過例子學習C++的第六篇,通過這個例子可以快速入門c++相關的語法。

1.問題描述

n 個人圍坐在一個圓桌周圍,現在從第 s 個人開始報數,數到第 m 個人,讓他出局;然後從出局的下一個人重新開始報數,數到第 m 個人,再讓他出局......,如此反覆直到所有人全部出局為止。

2.問題分析及用陣列求解

約瑟夫環是經典的演算法問題,如同“一千個讀者就有一千個哈姆雷特”,該問題每個人都有不同的解答。常見的有:陣列;單向迴圈連結串列;靜態連結串列;雙向連結串列;佇列;遞推公式 ......

首先簡化問題,從s=1開始數,通過陣列實現需要:

​ 陣列 bool a[1000],可能會浪費了大量的儲存空間;

​ 變數 t 從s=1開始數,指示當前陣列的位置;

​ 變數 f 記錄出局人數;

​ 變數 s 從1到m;

整個過程一個do-while迴圈即可實現,但理解起來卻是非常“拗口”。

程式碼如下:

#include<iostream>
using namespace std;
int n,m,s,f,t;
bool a[1000];
int main()
{
    cin>>n>>m;      //共n人,從1開始數,數到m出局
    for (int i=1;i<=n;++i){
        a[i]=false;
    }

    t=0;//從陣列a的a[1]開始...記錄陣列a的第t個位置 
    f=0;//記錄出局人數 
    s=0;//從1數到m,然後再從1數到m... 
   do
   {
        ++t;
        if (t==n+1) t=1;    //數到最後一個後,將t指向第一個 
        if (a[t]==false) ++s;   //第t個位置上有人則報數
        if (s==m)       //當前報的數是m
        {
            s=0;        //計數器清零
            cout<<t<<" ";   //出局人的編號
            a[t]=true;      //設定該位置已出局 
            f++;        //出局的人數加一 
        }
    } while(f!=n);      //所有的人都出局為止
 return 0;
}

程式執行效果如下圖:

3.陣列方式求解改進

下面,我們將s調整為鍵盤輸入,即從第s個人開始報數,實現程式碼如下:

#include<iostream>
using namespace std;
int n,m,s,f,t;
bool a[1000];
int main()
{
    cin>>n>>t>>m;       //共n人,從t開始數,數到m出局
    cout<<endl;
    for (int i=1;i<=n;++i){
        a[i]=false;
    }

    t = t -1;
    f=0;//記錄出局人數 
    s=0;//從1數到m,然後再從1數到m... 
   do
   {
        ++t;
        if (t==n+1) t=1;    //數到最後一個後,將t指向第一個 
        if (a[t]==false) ++s;   //第t個位置上有人則報數
        if (s==m)       //當前報的數是m
        {
            s=0;        //計數器清零
            cout<<t<<" ";   //出局人的編號
            a[t]=true;      //設定該位置已出局 
            f++;        //出局的人數加一 
        }
    } while(f!=n);      //所有的人都出局為止
 return 0;
}

程式執行效果如下圖:

4.靜態連結串列實現約瑟夫環

靜態連結串列,顧名思義就是用陣列模擬連結串列。為了程式的可讀性,特用一個函式表示約瑟夫環求解問題,呼叫的時候,只需要傳入n,s,m即可。由於陣列的長度n是動態生成的,故通過指標來生成陣列。

實現程式碼如下:

#include<iostream>
using namespace std;
 
//約瑟夫環問題 
void Josephus(int n,int s, int m) 
{
    cout<<n<<" "<<s<<" "<<m<<endl;
    int i,j,k;
    int *next= new int[n];
    //初始化靜態連結串列 
    for(i=0;i<n-1;++i){
        next[i] = i+1;
    } 
    next[n-1] = 0;
    
    //k初始化為s的前一個位置,陣列下標從0開始
    if(s==1){
        k = n-1;
    }else{
        k = s-2;
    }
    
    for(i=1;i<=n;++i){
        //找到出局人的前驅 
        for(j=1;j<m;++j){
            k=next[k];
        }   
        cout<<next[k]+1<<" ";//陣列下標從0開始,故需要+1 
        //數到m的人出列,刪除該元素
        next[k] = next[next[k]]; 
    }
}
int main(){
    Josephus(9,2,5);
    return 0;
}

程式執行後效果如下:

5.總結

本文中通過陣列、靜態連結串列實現了約瑟夫環。陣列方式實現,各個元素之間的“線性關係”未在資料結構中體現,需要通過變數t、f、s來分別指示當前陣列元素的位置、出局人數、計數1-m直到所有元素都“出局”為止。類似的,通過佇列實現,跟陣列實現邏輯上差不多。區別在於資料結構不同,但演算法一樣。

靜態連結串列方式實現,各個元素之間的“線性關係”通過next指示了,只需要k遍歷即可,理解起來更加直觀。類似的,通過單向迴圈連結串列、雙向連結串列實現跟靜態連結串列實現邏輯上差不多。區別在於資料結構不同,但演算法一樣。

至於通過遞推公式實現,從“計算機”角度看,一般難以“想到”該方法。

通過該例子,可以學習:

  • 指標;
  • 函式定義、呼叫;
  • 通過優化求解約瑟夫環,加深問題的理解。

本文從構思到完成,可謂是耗費了大量的心血。

如果您閱讀本文後哪怕有一丟丟收穫,請不要吝嗇你手中關注和點讚的權力,謝謝!

另外,如果需要相關程式碼,請留言,可以提供完整原始碼