1. 程式人生 > >經典例題|約瑟夫環多方法解決

經典例題|約瑟夫環多方法解決

本文章將用迴圈連結串列、陣列、遞迴以及迴圈方法對約瑟夫環問題進行講解。其中連結串列法和陣列法會對過程進行模擬,遞迴和迴圈將對約瑟夫環問題進行數學剖析。

問題描述

n個人圍成圈,依次編號為1、2、3、...、n,從1號開始依次報數,當報到m時,報m的人退出,下一個人重新從1報起,當報到m時,報m的人退出,如此迴圈下去,問最後剩下的那個人的編號是多少?

連結串列法

建立一個迴圈連結串列,節點的數值部分儲存整數1至n,將尾部節點連結到第一個節點,每次遍歷m-2步,把第m-1個節的指標域指向的節點資料打印出來,然後將m--1這個節點的指標域指向m的指標域指向的節點,再用free將m這個節點記憶體釋放.當只剩一個節點時,(只剩一個節點的判斷方法是:節點的指向節點自己,也就是p->next=p),節點的數值部分就是最後那個人的編號。
程式碼附上

#include<stdio.h>
#include<stdlib.h>
typedef struct list1{
    int data;
    struct list1 *next;
}list;                          //宣告一個連結串列節點
void func(int ,int ,list *);
void stamp(list *);
int main()
{
    int n,m;
    printf("請輸入總人數、出列序號\n");
    scanf("%d%d",&n,&m);
    list *l=NULL;
    list *k;
    for(int i=n;i>=1;i--)                           //建立連結串列
    {
        list *p=(list *)malloc(sizeof(list));
        if(i==n)        k=p;
        p->data=i;
        p->next=l;
        l=p;
    }
        k->next=l;                          //將尾節點連結到第一個節點
        func(n,m,l);
        return 0;
}
void func(int n,int m,list *l)
{
    int num=n;
    stamp(l);
    while(l->next!=l)
    {
        for(int i=1;i<m-1;i++)
        {
        l=l->next;
        }
        printf("*********%d號出列*********\n",l->next->data);
        l->next=l->next->next;
        l=l->next;
        num--;
        if(num>1) stamp(l);
        else printf("%d號選手勝出",l->data);
    }
}
void stamp(list *l)                             //每次排除一個人就輸出剩餘人數(方便理解過程)
{
    list *temp=l;
    printf("剩下的選手為\n");
    do
    {
        printf("%d ",temp->data);
        temp=temp->next;
    }while(temp!=l);
    putchar('\n');
} 

陣列法

和連結串列法相似,陣列法也是模擬過程.用i來模擬當前人物對應相應陣列下標,當i超出剩下人數時,i重歸於0,即回到第一個人,從而達到迴圈的效果,s模擬當前已經歷過的人數,當s==m時即淘汰當前人物,將陣列從i這個位置往前移動,實現刪除這個人的效果

#include<stdio.h>
void func(int n  ,int m );
int main()
{
    int m,n;
    printf("請輸入共有多少人,出列編號\n");
    scanf("%d%d",&n,&m);            //n人數,m報到出列的號碼
        func(n,m);
}
void func(int n,int m)
{
    int a[n],num=n,i,k;                     //num記錄當前剩餘人數
    for(i=0;i<n;i++)
        a[i]=i+1;
    i=0;    
    while(num>1)
    {
        int s=0;                                //記錄已經越過幾個人
        for(;;i++)
        {
            if(i==num)
                i=0;
            s++;
            if(s==m)    break;                  //執行刪除
        }
        printf("***********%d號出局***********\n",a[i]);
        for(int j=i;j<num;j++)                  //刪除當前人
            a[j]=a[j+1];                
        num--;
    }
    printf("%d號勝出\n",a[0]);             結束
}

數學法(遞迴.迴圈)

這個需要數學分析:
假如有6個人,編號為0,1,2,3,4,5,每次報到3的人出列(n=6,m=3);來模擬一下這個過程
第一次淘汰後 0,1,3,4,5 (1)
由於下一次是從3號開始我們可以改寫為 3,4,0,1,2 (2)
第二次淘汰後 3,4,0,1 (1)
同理:我們可以改寫為 0,1,2,3 (2)
第三次淘汰後 0,1,3 (1)
同理:我們可以改寫為 1,2,0 (2)
第四次淘汰後 0,1 (1)
同理:我們可以改寫為 0,1 (2)
第五次 1 (1)
改寫為 0 (2)
通過觀察,發現(1)式可由二式推匯出來 例如第五次 ((2)+3)%2
第四次 ((2)+3)%3
......
第一次 ((2)+3)%6
可以發現規律就是(1)式可以由((2)+m)%x x為本輪剩餘人數
這樣的話我們可以利用遞推來解決這個問題.無論你怎麼淘汰最後一個剩下的人在(2)式的情況下一定是0,所以可以利用這個規律不斷向前得到他原來的序號-1,因為我們是從0開始排序的,但是題目是從1開始排序的.驗證一下
(0+3)%2=1,(1+3)%3=1,(1+3)%4=0,(0+3)%5=3,(3+3)%6=0;0+1=1,所以剩下的人應該為1,大家可以用筆驗算是不是1,答案肯定是的.這樣就可以寫程式了.因為取餘可能會得到0這個序號,所以我們排序從0開始排,就不用進行其他轉化了

遞迴法

#include<stdio.h>
int func(int ,int );
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    printf("%d",func(n,m)+1);               //得到的數加1就是需要的序號
    return 0;
}
int func(int n,int m)
{
    if(n==1)                                //還剩一個人的時候返回0
        return 0;
    else return (func(n-1,m)+m)%n;      //還剩n個人時就返回((2)+m)%n這個數
}


執行結果,正確

迴圈法

我們利用遞迴由於不能及時清理現場會佔用大量的資源,所以資料較大時會給計算機帶來較大負荷,所以得換一個法子,就用迴圈將遞迴改造一下就行,因為迴圈一次清理一次現場,就不會出現上述現象

#include <stdio.h> 
int main()  
{  
    int n,m,i,s=0;
    while( scanf("%d%d",&n,&m)==2)
    {
        s=0;
        for (i=2; i<n; i++)             //遞迴往裡面進去,則迴圈從裡面出來,用i代表剩餘人數
            s=(s+m)%i;  
        printf ("%d",s+1);              //輸出結果
    }
    return 0 ;  
}  

在這裡插入圖片描述
執行結果,大家可以驗算一下,是沒有錯誤的.經過一番推理長程式碼就剩下了幾行,可見演算法是十分重要的,但是當題目要求輸出每一層被淘汰的人的話,這個程式就不適用的,可見程式碼各有各的好處,但是要根據題目要求來.
看了這麼幾種方法你學會了什麼呢,歡迎加入piu小屋c語言學習群 piu小屋c語言交流群進行交流.

piu小屋|全文結