約瑟夫環圖文詳解

想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
在這裏插入圖片描述
問題來源:

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

一,留下K-1個

41數字太大了,我們就以7爲例,來畫個圖看一下

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
我們再來看下代碼

1,數組的實現

 public static Integer[] solution(int count, int k) {
     Integer live[] = new Integer[Math.min(count, k - 1)];
     if (count < k) {
         int index = 0;
         while (index < count) {
             live[index++] = index;
         }
         return live;
     }
    List<Integer> mList = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
        mList.add(i + 1);
    }

    int point = 0;
    int number = 0;
    while (mList.size() >= k) {
        number++;
        if (point >= mList.size()) {
            point = 0;
        }
        if (number % k == 0) {
            mList.remove(point);
            continue;
        }
        point++;
    }
    return mList.toArray(live);
}

1, 3-9行表示如果k大於count就直接把所有人的編號都返回即可,不再刪除了。

2, 11-13行生成從1到count的所有值(包含1和count)

3, 16行number統計數量,在第22-25行如果統計的數量是k的倍數就把他移除。其實我們也可以在22行成立的時候讓number重新歸0。這裏使用的是對k求餘也是可以的。

4, 我們就用上面已知的兩組數據測試一下,當count等於41的時候結果是16和31,當count等於7的時候結果是1和4

    Util.printObjectArrays(solution(41, 3));
    System.out.println("---------------------");
    Util.printObjectArrays(solution(7, 3));

運行結果是

        16
        31
        ---------------------
        1
        4

結果完全正確。

數組的刪除會導致後面的元素都會往前移,頻繁的刪除效率肯定不是很高,其實我們還可以使用鏈表。因爲鏈表的刪除不需要移動後面的元素,效率還是比較高的。如果不使用鏈表,我們還可以把數組中刪除的元素用一個負數來填充,這樣也是可以的。我們來看下

2,數組實現的另一種方式

/**
  * @param count 總人數
  * @param k     每隔幾個人殺掉
  * @return
  */
 public static Integer[] solution(int count, int k) {
     Integer live[] = new Integer[Math.min(count, k - 1)];
     if (count < k) {
         int index = 0;
        while (index < count) {
            live[index++] = index;
        }
        return live;
    }
    List<Integer> mList = new ArrayList<>(count);
    for (int i = 0; i < count; i++) {
        mList.add(i + 1);
    }

    int point = 0;
    int number = 0;
    int total = count - k + 1;//記錄總共刪除的個數
    while (true) {
        if (total <= 0)
            break;
        if (point >= mList.size()) {
            point = 0;
        }
        if (mList.get(point) < 0) {
            point++;
            continue;
        }
        number++;
        if (number % k == 0) {
            mList.set(point, -1);//如果是第k個,就把他變爲負數
            total--;
            continue;
        }
        point++;
    }
    int index = 0;
    for (int i = 0; i < mList.size(); i++) {
        if (mList.get(i) > 0)
            live[index++] = mList.get(i);
    }
    return live;
}

第35行該刪除的我們沒有刪除,直接把他變爲-1。在29行統計的時候如果爲負數表示已經被刪除了,就直接跳過,執行下一輪循環。第42-45行把最後沒有被刪除的放到數組live中。

3,鏈表實現

一般來說鏈表的斷開要比數組的刪除效率要高一些,因爲數組刪除某個元素之後,它後面的元素都還要往前移。使用鏈表會更簡單一些,我們可以把它想象爲一圈人大家都手牽着手,然後再一個個報數,當報到k的時候就自動退出,退出的時候左右兩邊人的手要牽到一塊重新構成一個新的環,代碼很簡單,我們看下

 public static Integer[] solution(int count, int k) {
     Integer live[] = new Integer[Math.min(count, k - 1)];
     if (count < k) {
         int index = 0;
         while (index < count) {
             live[index++] = index;
         }
         return live;
     }
    LinkedList<Integer> mList = new LinkedList<>();
    for (int i = 0; i < count; i++) {
        mList.addLast(i + 1);
    }

    int point = 0;
    int number = 0;
    while (mList.size() >= k) {
        number++;
        if (point >= mList.size()) {
            point = 0;
        }
        if (number % k == 0) {
            mList.remove(point);
            continue;
        }
        point++;
    }
    return mList.toArray(live);
}

隊列實現:

除了上面說的數組和鏈表以外,我們還可以使用隊列。這個實現起來也非常簡單。我們把所有的元素全部入隊,然後再一個個出隊,出隊的時候記錄出隊的個數,如果不是第k個就讓他重新入隊,如果是第k個就不用了入隊了,然後下一個出隊的再重新從1開始計算。我們還是以7來畫個圖看一下。

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
我們先來看一下代碼,隊列就是用之前寫的3,常見數據結構-隊列中的雙端隊列。

/**
  * @param count 總人數
  * @param k     每隔幾個人殺掉
  * @return
  */
 public static Integer[] solution(int count, int k) {
     Integer live[] = new Integer[Math.min(count, k - 1)];
     if (count < k) {
         int index = 0;
        while (index < count) {
            live[index++] = index;
        }
        return live;
    }
    MyQueue<Integer> queue = new MyQueue<>(count + 1);
    for (int i = 0; i < count; i++) {
        queue.addLast(i + 1);
    }

    int number = 1;
    while (queue.size() >= k) {
        Integer item = queue.removeFirst();
        if (number % k == 0) {
            number = 1;
            continue;
        }
        queue.addLast(item);
        number++;
    }
    int index = 0;
    while (!queue.isEmpty()) {
        live[index++] = queue.removeFirst();
    }
    return live;
}

二,只留下一個

上面我們講的是每到第K個刪除,如果count大於等於k的話,最終會留下k-1個。但對這題還有另一個版本,就是無論多少個,最後只留下1個,就是說如果數量小於k個的時候我們繼續循環刪除,直到留下最後一個的爲止。原理和上面非常類似,只不過當刪除到最後小於K個的時候我們還要繼續循環即可。圖就不再畫了,我們就用最後雙端隊列這種實現來改一下。

1,雙端隊列解決

public static Integer solution(int count, int k) {
     MyQueue<Integer> queue = new MyQueue<>(count + 1);
     for (int i = 0; i < count; i++) {
         queue.addLast(i + 1);
     }
     int number = 1;
     while (queue.size() > 1) {
         Integer item = queue.removeFirst();
         if (number % k == 0) {
            number = 1;
            continue;
        }
        queue.addLast(item);
        number++;
    }
    return queue.removeFirst();
}

2,遞歸解決

我們用f(n,k)表示有n個人,第k個出列,最後列出的人的編號。

那麼f(n-1,k)就表示有n-1個人,第k個出列,最後列出的人的編號。

所以我們可以找到遞歸的公式f(n,k)=f(n-1,k)+k;也就是說n-1個人組成的環相對於n個人組成的環相當於順時針旋轉了k個單位。因爲是環形的,當超出環的大小的時候我們要對它求餘,所以爲了防止越界問題我們要這樣寫f(n,k)=(f(n-1,k)+k)%n。

當f(1,k)的時候就表示剩下最後一個元素了,我們直接返回即可。

我們來看下代碼

public static int solution(int n, int k) {
    return helper(n, k) + 1;
}

public static int helper(int n, int m) {
    if (n == 1)
        return 0;
    return (helper(n - 1, m) + m) % n;
}

因爲人的編號是從1開始的,所以這裏要加1。當然我們還可以再來改一下,這樣就不用在加1,就可以直接返回了。

public static int solution(int n, int k) {
    if (n == 1)
        return n;
    return (solution(n - 1, k) + k - 1) % n + 1;
}

3,非遞歸寫法

看明白了上面的遞歸的思路,我們還可以把它改爲非遞歸的寫法

public static int solution(int n, int k) {
    int m = 0;
    for (int i = 2; i <= n; i++) {
        m = (m + k) % i;
    }
    return m + 1;
}

他的原理是這樣的,從前往後推,如果當n=i的時候,最終留下的是m,那麼當n=i+1的時候,最終留下的就是m+k,考慮到m+k可能大於環的長度,所以要對m+k進行求餘,結果就是(m+k)%i,一直循環到i等於n就是最終結果。

總結:

這題只要是學過編程的大多數應該都聽過,無論是使用數組,鏈表,還是隊列都很容易解決,具有一定的代表性,希望大家能夠熟練掌握。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章