約瑟夫環問題算是很經典的題了,估計你們都據說過,而後我就在一次筆試中遇到了,下面我就用 3 種方法來詳細講解一下這道題,最後一種方法學了以後保證讓你可讓你裝逼。面試
問題描述:編號爲 1-N 的 N 個士兵圍坐在一塊兒造成一個圓圈,從編號爲 1 的士兵開始依次報數(1,2,3...這樣依次報),數到 m 的 士兵會被殺死出列,以後的士兵再從 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);函數
學過鏈表的人,估計都會用鏈表來處理約瑟夫環問題,用鏈表來處理其實和上面處理的思路差很少,只是用鏈表來處理的時候,對於被選中的編號,再也不是作標記,而是直接移除,由於從鏈表移除一個元素的時間複雜度很低,爲 O(1)。固然,上面數組的方法你也能夠採用移除的方式,不過數組移除的時間複雜度爲 O(n)。因此採用鏈表的解決方法以下:學習
一、先建立一個環形鏈表來存放元素:this
二、而後一邊遍歷鏈表一遍刪除,直到鏈表只剩下一個節點,我這裏就不所有演示了
代碼以下:
// 定義鏈表節點 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; }
臥槽,之後面試官讓你手寫約瑟夫問題,你就扔這一行代碼給它。
不過那次筆試時,並無用遞歸的方法作,而是用鏈表的方式作,,,,,那時,不知道原來還能用一行代碼搞定的,,,,歡迎各位大佬提供半行代碼搞定的方法!
一、給俺點個讚唄,可讓更多的人看到這篇文章,順便激勵下我,嘻嘻。
二、老鐵們,關注個人原創微信公衆號「帥地玩編程」,專一於寫算法 + 計算機基礎知識(計算機網絡+ 操做系統+數據庫+Linux)。
保存讓你看完有所收穫,不信你打我。後臺回覆『電子書』送你一份精選電子書大禮包,包含各種技能的優質電子書。
做者:你們好,我是帥地,從大學、校招一路走來,深知算法,計算機基礎知識的重要性,因此申請了一個微星公衆號『帥地玩編程』,專業於寫這些底層知識,提高咱們的內功,帥地期待你的關注,和我一塊兒學習。 轉載說明:未得到受權,禁止轉載