【年前最後一波裝逼】記一次阿里面試,我是如何用一行代碼解決約瑟夫環問題的

約瑟夫環問題算是很經典的題了,估計你們都據說過,而後我就在一次筆試中遇到了,下面我就用 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)。

保存讓你看完有所收穫,不信你打我。後臺回覆『電子書』送你一份精選電子書大禮包,包含各種技能的優質電子書。

做者簡潔

做者:你們好,我是帥地,從大學、校招一路走來,深知算法計算機基礎知識的重要性,因此申請了一個微星公衆號『帥地玩編程』,專業於寫這些底層知識,提高咱們的內功,帥地期待你的關注,和我一塊兒學習。 轉載說明:未得到受權,禁止轉載

相關文章
相關標籤/搜索