Leetcode - 面試題62-- 圓圈中最後剩下的數字

圓圈中最後剩下的數字

題目描述

0,1,n-1這n個數字排成一個圓圈,從數字0開始,每次從這個圓圈裏刪除第m個數字。求出這個圓圈裏剩下的最後一個數字。

例如,0、1、2、3、4這5個數字組成一個圓圈,從數字0開始每次刪除第3個數字,則刪除的前4個數字依次是2、0、4、1,因此最後剩下的數字是3。

  • 題目限制
    • 1 <= n <= 10^5
    • 1 <= m <= 10^6

示例

  1. 示例1
輸入: n = 5, m = 3
輸出: 3
  1. 示例2
輸入: n = 10, m = 17
輸出: 2

解題思路

隊列模擬

看到這個題,我第一眼想到的是使用隊列去模擬題目所說的情況,先將0-n-1個數字加入到隊列中,開始數數,從0開始到m-1,取出隊列重新加入到隊列尾部,刪除第m個,不斷重複這個過程,直到隊列中只剩1個元素,直接返回這個剩餘的元素即可。

很不幸,這種方法超時了。。。。

var lastRemaining = function(n, m) {
    let queue = new Queue()
    let index = 0
    for (let i=0; i<n; i++) {
        queue.enqueue(i)
    }
    while (queue.size() > 1) {
    /**
     * 開始數數字
     * 不是m的時候,重新加入到隊列的末尾
     * 是m的時候,將其從隊列刪除
    */
    for (let i=0; i<m-1; i++) {
      queue.enqueue(queue.dequeue())
    }
    // 是m的時候直接刪除
    queue.dequeue()
  }
  let end = queue.front()
  return end
};

這種方法不成功是顯而易見的,首先,事件複雜度O(nm),再看一下數據範圍,不超時纔怪!!!!

鏈表模擬

與隊列模擬相同,只不過是換一下數據結構而已!!

使用數組求解

首先,對於本題,假設刪除的位置是index位置的元素,那麼下一個要刪除的是index + m位置的元素,但是由於當前位置的數字刪除了,後面的數字會前移一位,所以實際的下一次刪除的位置是index + m - 1,由於是一個環,所以可以取模:

(index + m - 1) mod n來從頭開始

// 通過
var lastRemaining = function(n, m) {
    let arr = []
   for (let i=0; i<n; i++) {
       arr.push(i)
   }
   let id = 0
   while(n > 1){
       id = (id + m - 1) % n
       arr.splice(id,1)
       n-- 
   }
   return arr[0]
};

這種方法過是僥倖過了,但耗時有點大

在這裏插入圖片描述

數學+遞歸 – 官方答案

  1. 思路

    題目中的要求可以表述爲:給定一個長度爲 n 的序列,每次向後數 m 個元素並刪除,那麼最終留下的是第幾個元素?

    這個問題很難快速給出答案。但是同時也要看到,這個問題似乎有拆分爲較小子問題的潛質:如果我們知道對於一個長度 n - 1 的序列,留下的是第幾個元素,那麼我們就可以由此計算出長度爲 n 的序列的答案。

  2. 算法

    我們將上述問題建模爲函數 f(n, m),該函數的返回值爲最終留下的元素的序號。

    首先,長度爲 n 的序列會先刪除第 m % n 個元素,然後剩下一個長度爲 n - 1 的序列。那麼,我們可以遞歸地求解 f(n - 1, m),就可以知道對於剩下的 n - 1 個元素,最終會留下第幾個元素,我們設答案爲 x = f(n - 1, m)。

    由於我們刪除了第 m % n 個元素,將序列的長度變爲 n - 1。當我們知道了 f(n - 1, m) 對應的答案 x 之後,我們也就可以知道,長度爲 n 的序列最後一個刪除的元素,應當是從 m % n 開始數的第 x 個元素。因此有 f(n, m) = (m % n + x) % n = (m + x) % n

    我們遞歸計算 f(n, m), f(n - 1, m), f(n - 2, m), … 直到遞歸的終點 f(1, m)。當序列長度爲 1 時,一定會留下唯一的那個元素,它的編號爲 0。

    /**
    class Solution {
        int f(int n, int m) {
            if (n == 1)
                return 0;
            int x = f(n - 1, m);
            return (m + x) % n;
        }
    public:
        int lastRemaining(int n, int m) {
            return f(n, m);
        }
    //時間複雜度:O(n)O(n),需要求解的函數值有 nn 個。
    //空間複雜度:O(n)O(n),函數的遞歸深度爲 nn,需要使用 O(n)O(n) 的棧空間
    */
    //官方C++代碼毫無問題,使用JS後 RangeError: Maximum call stack size exceeded報錯!!!
    function f (n, m) {
       if (n === 1) {
           return 0
       } 
       let x = f(n-1, m)
       return (m+x) % n
    }
    function lastRemaining(n, m) {
        return f(n,m)
    }
    

    意思是遞歸次數太多導致內存消耗過大!!!!好尷尬!!!!

數學+迭代 – 官方答案

  1. 避免遞歸使用棧空間。

    // 完美通過
    function lastRemaining(n, m) {
        let f = 0;
        for (let i = 2; i != n + 1; ++i)
            f = (m + f) % i;
        return f;
    }
    //時間複雜度:O(n)O(n),需要求解的函數值有 nn 個。
    //空間複雜度:O(1)O(1),只使用常數個變量。反推法
    

反推法

  1. 思路

    n個人編號0,1,2,…,n-1,每數m次刪掉一個人

    假設有函數f(n)表示n個人最終剩下人的編號

    n個人刪掉1個人後可以看做n-1的狀態,不過有自己的編號。

    n個人刪掉的第一個人的編號是(m-1)%n,那麼n個人時刪掉第一個人的後面那 個人(m-1+1)%n一定是n-1個人時候編號爲0的那個人,即n個人時的編號m%n(這個編號是對於n個人來考慮的),n-1個人時編號爲i的人就是n個人時(m+i)%n

    所以f(n)=(m+f(n-1))%n
    f(1)=0,因爲1個人時只有一個編號0

    因此可以將人數從2反推到n:

        var lastRemaining = function (n, m) {
          let ans = 0;
          for (let i = 2; i <= n; i++) {
            ans = (ans + m) % i;
          }
          return ans;
    	 }
    

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