1. 程式人生 > 程式設計 >JavaScript三種方法解決約瑟夫環問題的方法

JavaScript三種方法解決約瑟夫環問題的方法

目錄
  • 概述
  • 問題描述
  • 迴圈連結串列
  • 有序陣列
  • 數學遞迴
  • 總結

概述

約瑟夫環問題又稱約瑟夫殺人問題或丟手絹問題,是一道經典的演算法問題。問題描述也有很多變式,但大體的解題思路是相同的。本篇將以迴圈連結串列、有序陣列、數學遞迴三種方式來解決約瑟夫環問題。

問題描述

先來看一下什麼是約瑟夫環問題?

在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡為止。然而Josephus 和他的朋友並不想遵從。首先從一個人開始,越過k-2個人(因為第一個人已經被越過),並殺掉第k個人。接著,再越過k-1個人,並殺掉第k個人。這個過程沿著圓圈一直進行,直到最終只剩下一個人留下,這個人就可以繼續活著。問題是,給定了和,一開始要站在什麼地方才能避免被處決。

到了今天約瑟夫環問題的描述一般變成了:

在一間房中有N個人,所有人圍成一圈順時針開始報數,每次報到M的人退出遊戲。退出遊戲的下一個人再次重新開始報數,報到M的人再退出,依此迴圈,直到遊戲只剩下最後一個人,則最後一個人的編號是多少?

迴圈連結串列

迴圈連結串列的解題思路比較簡單,只需要將問題描述轉換成程式碼即可。房間中的N個人組成一個長度為N的連結串列,首尾相接組成迴圈連結串列。列表中的每一項的值即為每個人的編號,在數到M時將該項(記為x)剔除,即該項的前一項的next不再指向x,而是x.next。依此規律遍歷連結串列,直至連結串列中只剩下一項。

下面看一下程式碼實現。

function createList(num) {
    //連結串列節點的資料結構
    function createNode(value) {
        return {
            value: value,next: ''
        }
    }
    //連結串列頭節點
    let head = createNode(1);
    let node = head;
    //自頭節點之後建立節點之間的關聯關CDjlXxVDY
系 for (let i = 2; i <= num; i++) { node.next = createNode(i); node = node.next; } //最後一個節點指向頭節點,構成迴圈連結串列 node.next = head; return head; } function deleteListNode(num,nth) { //建立資料長度為num的迴圈連結串列 let node = createList(num); www.cppcns.com//連結串列長度>1時,繼續下一輪 while (num > 1) { for (let i = 1; i <= nth - 1; i++) { if (i == nth - 1) { //i為nth-1,則node.next即為第nth個節點。CDjlXxVDY
剔除node.next node.next = node.next.next; //連結串列長度-- num--; } node = node.next; } } //剩餘的最後一個節點的value值即為最後一人編號 return node.value } //deleteListNode(m,n)即可得到最終結果

有序陣列

用有序陣列來模擬迴圈連結串列,將陣列第一次遍歷剔除完成後剩餘的陣列項組成一個新的陣列,再對新陣列進行新一輪的遍歷剔除,依此迴圈,直到陣列長度為1。

下面看一下程式碼實現。

function jhonRing(num,nth) {
    let arr = [];
    //建立有序陣列
    for (let i = 1; i <= num; i++) {
        arr[i - 1] = i;
    }
    //當陣列長度大於nth時,陣列不用迴圈遍歷也可找到需要剔除的資料
    while (arr.length >= nth) {
        let newArr = [];
        //將陣列末尾剩下的資料轉移到新陣列的前方,新一輪遍歷從生於的資料開始
        let times = arr.length - arr.length % nth;
        newArr = arr.slice(times)
        arr.slice(0,times).map((item,index) => {
            //將除了剔除資料之外的其他資料加入新的陣列
            if ((index + 1) % nth !== 0) {
                newArr.push(item)
            }
        })
        arr = newArr;
    }
    //當陣列長度小於nth時,陣列需要迴圈遍歷才能找到要剔除的資料,通過取餘操作可減少遍歷的次數
    while (arr.length > 1) {
        //取餘獲取要剔除的資料的下標
        let index = nth % arr.length - 1;
        //剔除資料的後半部分與前半部分組成新的陣列
        let newArr = arr.slice(index + 1).concat(arr.slice(0,index));
        arr = newArr;
    }
}
//jhonRing(num,nth)即可得到最終結果

用有序陣列模擬連結串列的操作看上去跟連結串列差不多,時間複雜度看上去也一樣,甚至程式碼也比不上鍊表簡潔,但是為什麼仍然要把這個方式提出來呢?這種方法的優勢體現在M>>N的情況下,有序連結串列通過取餘的方式有效的減少了迴圈遍歷陣列的次數。以N為3,M為100為例,連結串列需要迴圈遍歷100/3+1次,而有序陣列則根據取餘操作的結果100/3-1=0,直接得到了要剔除的資料下標為0。

數學遞迴

用數學問題求解約瑟夫環問題時極易找不到一條有效的規律總結路線,下面以M=3舉例講述一下總結規律時走過的彎路。(可跳過無效的思路,直接閱讀下方的有效思路)

比較多次資料量較小時的結果(❌)

N=1時,f(1,3)=1;
N=2時,f(2,3)=2;
N=3時,f(3,3)=2;
N=4時,f(4,3)=1;
N=5時,f(5,3)=4;
N=6時,f(6,3)=1;
N=7時,f(7,3)=4;
N=8時,f(8,3)=7;
N=9時,f(9,3)=1;
N=10時,f(10,3)=4;

通過舉例發現,最終結果並不總是在某幾個數之間,除了1,2,4以外還出現7,則之後的結果也有可能有類似的情況,即最終結果並不總是侷限於某一個數之間。f(3,3) f(6,3) f(9,3)等N為M的倍數的情況並沒有得到相同的結果,可見最終結果與3的倍數之間並不存在某種特殊聯絡。因此通過這種積累比較資料量較小時的結果來尋找規律的方案不合適。

比較前幾次剔除資料之間的關係(❌)

//將N個人自1-N進行編號
1,2,3,4........N-2,N-1,N
//第一次剔除的編號為M或M%N,可總結為M%N,記為K。則第二次剔除時的序列變為
K+1,K+2,.......N,1,2......K-1
//則第二次剔除的編號為K+M%(N-1) || M%(N-1)-(N-K-1)
依此得到規律
當N-(K+1)>M%www.cppcns.com(N-1)時,f(n)=f(n-1)+M%(N-(n-1))
當N-(K+1)<M%(N-1)時,f(n)=M%(N-(n-1))-(N-f(n-1)-1)

依此規律計算會發現,沒有進行第二圈遍歷時得到的結果是正確的,遍歷進入第二圈之後的結果錯誤。根本原因在於進入第二圈之後的資料不再連續,第一圈遍歷時剔除出的資料會影響第二圈遍歷的結果,故此方案不合適。

自上而下分析問題,自下而上解決問題(✅)

用遞迴的思路去分析約瑟夫環問題。以N=10,M=3為例分析。

//將10個人從0開始編號(稍後解釋為什麼不能從1開始編號)
0,4,5,6,7,8,9
//將最後一個出列的人的編號記做f(10,3)
//在10個人編號後,第一個人出列後,得到新的佇列及編號
3,9,1
//為了使新佇列的編號連續,我們可以將新佇列記做A,寫作
3,10%10,11%10
//若將一個9人普通佇列記做B,寫作0,8的最終結果記做f(9,3),則新佇列A的結果則可以表示為(f(9,3)+3)%10
//9人佇列A是10人佇列剔除一人後得到的,則9人佇列按N=9,M=3,初始編號為3的規則進行遊戲後得到的結果必然與N=10,M=3,初始編號為0的佇列最後出列的人相同。
//故10人佇列最後出列的人編號與9人佇列A出列的人編號之間存在關係f(10,3)=(f(9,3)+3)%10

基於以上可以得到結論f(N,M)=(f(N-1,M)+M)%N。則最終編號的結果即為f(N,M)+1。
為什麼編號不能從1開始呢?因為取餘操作的返回結果是從0開始。

function Josephus(num,nth){
    if(num==1){
        return 0;
    }else{
        return (Josephus(num-1,nth)+nth)%num
    }
}
//Josephus(N,M)+1即為最終編號

對遞迴函式做一下優化

function JosephusR(num,nth){
    let value = 0;
    for(let i=1;i<=num;i++){
        //此處為對i取餘,上述遞迴中num也是在逐漸變小的,所以此處為i而非num
        value=(value+nth)%i;
    }
    return value+1;
}
//JosephusR(N,M)即為最終編號

總結

通過數學遞迴方式的優化,將約瑟夫環問題的時間複雜度從最初的O(mn)降低到O(n)。通過迴圈連結串列的方式來解決問題確實是最快最容易想到的思路,但是這種數學遞迴的方式對解決此類演算法問題來說更有思考的價值。

到此這篇關於三種方法解決約瑟夫環問題的方法的文章就介紹到這了,更多相關Script 約瑟夫環內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!