'一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的'

計算複雜性理論 苦逼的碼農 2019-07-16
"

約瑟夫環問題算是很經典的題了,估計大家都聽說過,然後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了之後保證讓你可以讓你裝逼。

問題描述:編號為 1-N 的 N 個士兵圍坐在一起形成一個圓圈,從編號為 1 的士兵開始依次報數(1,2,3…這樣依次報),數到 m 的 士兵會被殺死出列,之後的士兵再從 1 開始報數。直到最後剩下一士兵,求這個士兵的編號。

1、方法一:數組

在大一第一次遇到這個題的時候,我是用數組做的,我猜絕大多數人也都知道怎麼做。方法是這樣的:

用一個數組來存放 1,2,3 … n 這 n 個編號,如圖(這裡我們假設n = 6, m = 3)

"

約瑟夫環問題算是很經典的題了,估計大家都聽說過,然後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了之後保證讓你可以讓你裝逼。

問題描述:編號為 1-N 的 N 個士兵圍坐在一起形成一個圓圈,從編號為 1 的士兵開始依次報數(1,2,3…這樣依次報),數到 m 的 士兵會被殺死出列,之後的士兵再從 1 開始報數。直到最後剩下一士兵,求這個士兵的編號。

1、方法一:數組

在大一第一次遇到這個題的時候,我是用數組做的,我猜絕大多數人也都知道怎麼做。方法是這樣的:

用一個數組來存放 1,2,3 … n 這 n 個編號,如圖(這裡我們假設n = 6, m = 3)

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後不停著遍歷數組,對於被選中的編號,我們就做一個標記,例如編號 arr[2] = 3 被選中了,那麼我們可以做一個標記,例如讓 arr[2] = -1,來表示 arr[2] 存放的編號已經出局的了。

"

約瑟夫環問題算是很經典的題了,估計大家都聽說過,然後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了之後保證讓你可以讓你裝逼。

問題描述:編號為 1-N 的 N 個士兵圍坐在一起形成一個圓圈,從編號為 1 的士兵開始依次報數(1,2,3…這樣依次報),數到 m 的 士兵會被殺死出列,之後的士兵再從 1 開始報數。直到最後剩下一士兵,求這個士兵的編號。

1、方法一:數組

在大一第一次遇到這個題的時候,我是用數組做的,我猜絕大多數人也都知道怎麼做。方法是這樣的:

用一個數組來存放 1,2,3 … n 這 n 個編號,如圖(這裡我們假設n = 6, m = 3)

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後不停著遍歷數組,對於被選中的編號,我們就做一個標記,例如編號 arr[2] = 3 被選中了,那麼我們可以做一個標記,例如讓 arr[2] = -1,來表示 arr[2] 存放的編號已經出局的了。

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後就按照這種方法,不停著遍歷數組,不停著做標記,直到數組中只有一個元素是非 -1 的,這樣,剩下的那個元素就是我們要找的元素了。我演示一下吧:

"

約瑟夫環問題算是很經典的題了,估計大家都聽說過,然後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了之後保證讓你可以讓你裝逼。

問題描述:編號為 1-N 的 N 個士兵圍坐在一起形成一個圓圈,從編號為 1 的士兵開始依次報數(1,2,3…這樣依次報),數到 m 的 士兵會被殺死出列,之後的士兵再從 1 開始報數。直到最後剩下一士兵,求這個士兵的編號。

1、方法一:數組

在大一第一次遇到這個題的時候,我是用數組做的,我猜絕大多數人也都知道怎麼做。方法是這樣的:

用一個數組來存放 1,2,3 … n 這 n 個編號,如圖(這裡我們假設n = 6, m = 3)

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後不停著遍歷數組,對於被選中的編號,我們就做一個標記,例如編號 arr[2] = 3 被選中了,那麼我們可以做一個標記,例如讓 arr[2] = -1,來表示 arr[2] 存放的編號已經出局的了。

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後就按照這種方法,不停著遍歷數組,不停著做標記,直到數組中只有一個元素是非 -1 的,這樣,剩下的那個元素就是我們要找的元素了。我演示一下吧:

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

這種方法簡單嗎?思路簡單,但是編碼卻沒那麼簡單,臨界條件特別多,每次遍歷到數組最後一個元素的時候,還得重新設置下標為 0,並且遍歷的時候還得判斷該元素時候是否是 -1。感興趣的可以動手寫一下代碼,用這種數組的方式做,千萬不要覺得很簡單,編碼這個過程還是挺考驗人的。

這種做法的時間複雜度是 O(n * m), 空間複雜度是 O(n);

2、方法二:環形鏈表

學過鏈表的人,估計都會用鏈表來處理約瑟夫環問題,用鏈表來處理其實和上面處理的思路差不多,只是用鏈表來處理的時候,對於被選中的編號,不再是做標記,而是直接移除,因為從鏈表移除一個元素的時間複雜度很低,為 O(1)。當然,上面數組的方法你也可以採用移除的方式,不過數組移除的時間複雜度為 O(n)。所以採用鏈表的解決方法如下:

1、先創建一個環形鏈表來存放元素:

"

約瑟夫環問題算是很經典的題了,估計大家都聽說過,然後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了之後保證讓你可以讓你裝逼。

問題描述:編號為 1-N 的 N 個士兵圍坐在一起形成一個圓圈,從編號為 1 的士兵開始依次報數(1,2,3…這樣依次報),數到 m 的 士兵會被殺死出列,之後的士兵再從 1 開始報數。直到最後剩下一士兵,求這個士兵的編號。

1、方法一:數組

在大一第一次遇到這個題的時候,我是用數組做的,我猜絕大多數人也都知道怎麼做。方法是這樣的:

用一個數組來存放 1,2,3 … n 這 n 個編號,如圖(這裡我們假設n = 6, m = 3)

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後不停著遍歷數組,對於被選中的編號,我們就做一個標記,例如編號 arr[2] = 3 被選中了,那麼我們可以做一個標記,例如讓 arr[2] = -1,來表示 arr[2] 存放的編號已經出局的了。

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後就按照這種方法,不停著遍歷數組,不停著做標記,直到數組中只有一個元素是非 -1 的,這樣,剩下的那個元素就是我們要找的元素了。我演示一下吧:

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

這種方法簡單嗎?思路簡單,但是編碼卻沒那麼簡單,臨界條件特別多,每次遍歷到數組最後一個元素的時候,還得重新設置下標為 0,並且遍歷的時候還得判斷該元素時候是否是 -1。感興趣的可以動手寫一下代碼,用這種數組的方式做,千萬不要覺得很簡單,編碼這個過程還是挺考驗人的。

這種做法的時間複雜度是 O(n * m), 空間複雜度是 O(n);

2、方法二:環形鏈表

學過鏈表的人,估計都會用鏈表來處理約瑟夫環問題,用鏈表來處理其實和上面處理的思路差不多,只是用鏈表來處理的時候,對於被選中的編號,不再是做標記,而是直接移除,因為從鏈表移除一個元素的時間複雜度很低,為 O(1)。當然,上面數組的方法你也可以採用移除的方式,不過數組移除的時間複雜度為 O(n)。所以採用鏈表的解決方法如下:

1、先創建一個環形鏈表來存放元素:

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

2、然後一邊遍歷鏈表一遍刪除,直到鏈表只剩下一個節點,我這裡就不全部演示了

"

約瑟夫環問題算是很經典的題了,估計大家都聽說過,然後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了之後保證讓你可以讓你裝逼。

問題描述:編號為 1-N 的 N 個士兵圍坐在一起形成一個圓圈,從編號為 1 的士兵開始依次報數(1,2,3…這樣依次報),數到 m 的 士兵會被殺死出列,之後的士兵再從 1 開始報數。直到最後剩下一士兵,求這個士兵的編號。

1、方法一:數組

在大一第一次遇到這個題的時候,我是用數組做的,我猜絕大多數人也都知道怎麼做。方法是這樣的:

用一個數組來存放 1,2,3 … n 這 n 個編號,如圖(這裡我們假設n = 6, m = 3)

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後不停著遍歷數組,對於被選中的編號,我們就做一個標記,例如編號 arr[2] = 3 被選中了,那麼我們可以做一個標記,例如讓 arr[2] = -1,來表示 arr[2] 存放的編號已經出局的了。

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

然後就按照這種方法,不停著遍歷數組,不停著做標記,直到數組中只有一個元素是非 -1 的,這樣,剩下的那個元素就是我們要找的元素了。我演示一下吧:

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

這種方法簡單嗎?思路簡單,但是編碼卻沒那麼簡單,臨界條件特別多,每次遍歷到數組最後一個元素的時候,還得重新設置下標為 0,並且遍歷的時候還得判斷該元素時候是否是 -1。感興趣的可以動手寫一下代碼,用這種數組的方式做,千萬不要覺得很簡單,編碼這個過程還是挺考驗人的。

這種做法的時間複雜度是 O(n * m), 空間複雜度是 O(n);

2、方法二:環形鏈表

學過鏈表的人,估計都會用鏈表來處理約瑟夫環問題,用鏈表來處理其實和上面處理的思路差不多,只是用鏈表來處理的時候,對於被選中的編號,不再是做標記,而是直接移除,因為從鏈表移除一個元素的時間複雜度很低,為 O(1)。當然,上面數組的方法你也可以採用移除的方式,不過數組移除的時間複雜度為 O(n)。所以採用鏈表的解決方法如下:

1、先創建一個環形鏈表來存放元素:

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

2、然後一邊遍歷鏈表一遍刪除,直到鏈表只剩下一個節點,我這裡就不全部演示了

一道阿里筆試題:我是如何用一行代碼解決約瑟夫環問題的

代碼如下:

// 定義鏈表節點
class Node{
int date;
Node next;
public Node(int date) {
this.date = date;
}
}

核心代碼

 public static int solve(int n, int m) {
if(m == 1 || n < 2)
return n;
// 創建環形鏈表
Node head = createLinkedList(n);
// 遍歷刪除
int count = 1;
Node cur = head;
Node pre = null;//前驅節點
while (head.next != head) {
// 刪除節點
if (count == m) {
count = 1;
pre.next = cur.next;
cur = pre.next;
} else {
count++;
pre = cur;
cur = cur.next;
}
}
return head.date;
}
static Node createLinkedList(int n) {
Node head = new Node(1);
Node next = head;
for (int i = 2; i <= n; i++) {
Node tmp = new Node(i);
next.next = tmp;
next = next.next;
}
// 頭尾串聯
next.next = head;
return head;
}

這種方法估計是最多人用的,時間複雜度為 O(n * m),空間複雜度是 O(n)。

還有更好的方法嗎?答有,請往下看

方法三:遞歸

其實這道題還可以用遞歸來解決,遞歸是思路是每次我們刪除了某一個士兵之後,我們就對這些士兵重新編號,然後我們的難點就是找出刪除前和刪除後士兵編號的映射關係

我們定義遞歸函數 f(n,m) 的返回結果是存活士兵的編號,顯然當 n = 1 時,f(n, m) = 1。假如我們能夠找出 f(n,m) 和 f(n-1,m) 之間的關係的話,我們就可以用遞歸的方式來解決了。我們假設人員數為 n, 報數到 m 的人就自殺。則剛開始的編號為

1

m - 2

m - 1

m

m + 1

m + 2

n

進行了一次刪除之後,刪除了編號為 m 的節點。刪除之後,就只剩下 n - 1 個節點了,刪除前和刪除之後的編號轉換關係為:

刪除前 --- 刪除後

… --- …

m - 2 --- n - 2

m - 1 --- n - 1

m ---- 無(因為編號被刪除了)

m + 1 --- 1(因為下次就從這裡報數了)

m + 2 ---- 2

… ---- …

新的環中只有 n - 1 個節點。且刪除前編號為 m + 1, m + 2, m + 3 的節點成了刪除後編號為 1, 2, 3 的節點。

假設 old 為刪除之前的節點編號, new 為刪除了一個節點之後的編號,則 old 與 new 之間的關係為 old = (new + m - 1) % n + 1。

注:有些人可能會疑惑為什麼不是 old = (new + m ) % n 呢?主要是因為編號是從 1 開始的,而不是從 0 開始的。如果 new + m == n的話,會導致最後的計算結果為 old = 0。所以 old = (new + m - 1) % n + 1.

這樣,我們就得出 f(n, m) 與 f(n - 1, m)之間的關係了,而 f(1, m) = 1.所以我們可以採用遞歸的方式來做。代碼如下:

int f(int n, int m){
if(n == 1) return n;
return (f(n - 1, m) + m - 1) % n + 1;
}

我去,兩行代碼搞定,而且時間複雜度是 O(n),空間複雜度是O(n),牛逼!那如果你想跟別人說,我想一行代碼解決約瑟夫問題呢?答是沒問題的,如下:

int f(int n, int m){
return n == 1 ? n : (f(n - 1, m) + m - 1) % n + 1;
}

臥槽,以後面試官讓你手寫約瑟夫問題,你就扔這一行代碼給它。

總結

不過那次筆試時,並沒有用遞歸的方法做,而是用鏈表的方式做,,,,,那時,不知道原來還能用一行代碼搞定的,,,,歡迎各位大佬提供半行代碼搞定的方法!

"