本文參考自《劍指offer》一書,代碼采用Java語言。
題目
0, 1, …, n-1這n個數字排成一個圓圈,從數字0開始每次從這個圓圈里刪除第m個數字。求出這個圓圈里剩下的最后一個數字。
思路
方法一:采用鏈表來存放數據,每次對長度取余來實現循環
將所有數字放入LinkedList鏈表中(LinkedList比ArrayList更適合增刪操作)。假設當前刪除的結點下標為removeIndex,則下一個要刪除的結點的下標為:(removeIndex+m-1)%list.size(),通過取余符號可以實現類型循環的操作。
注:沒必要用循環鏈表,反而會更麻煩了。
方法二:數學推導規律
n個數字的圓圈,不斷刪除第m個數字,我們把最后剩下的數字記為f(n,m)。
n個數字中第一個被刪除的數字是(m-1)%n, 我們記作k,k=(m-1)%n。
那么剩下的n-1個數字就變成了:0,1,……k-1,k+1,……,n-1,我們把下一輪第一個數字排在最前面,並且將這個長度為n-1的數組映射到0~n-2。
原始數字:k+1,……, n-1, 0, 1,……k-1
映射數字:0 ,……,n-k-2, n-k-1, n-k,……n-2
把映射數字記為x,原始數字記為y,那么映射數字變回原始數字的公式為 y=(x+k+1)%n。
在映射數字中,n-1個數字,不斷刪除第m個數字,由定義可以知道,最后剩下的數字為f(n-1,m)。我們把它變回原始數字,由上一個公式可以得到最后剩下的原始數字是(f(n-1,m)+k+1)%n,而這個數字就是也就是一開始我們標記為的f(n,m),所以可以推得遞歸公式如下:
f(n,m) =(f(n-1,m)+k+1)%n
將k=(m-1)%n代入,化簡得到:
f(n,m) =(f(n-1,m)+m)%n
f(1,m) = 0
代碼中可以采用循環或者遞歸的方法實現該遞歸公式。時間復雜度為O(n),空間復雜度為O(1)。
測試算例
1.功能測試(m大於/小於/等於n)
2.特殊測試(n、m<=0)
3.性能測試(n=4000,n=997)
Java代碼
//題目:0, 1, …, n-1這n個數字排成一個圓圈,從數字0開始每次從這個圓圈里
//刪除第m個數字。求出這個圓圈里剩下的最后一個數字。
public class LastNumberInCircle {
/*
* 方法一:采用推導出來的方法
*/
public int LastRemaining_Solution(int n, int m) {
if(n<1 || m<1)
return -1; //出錯
int last=0;
for(int i=2;i<=n;i++){
last=(last+m)% i; //這里是i不是n!!!
}
return last;
}
/*
* 方法二:采用鏈表來存放,每次對長度取余來實現循環
*/
public int LastRemaining_Solution2(int n, int m) {
if(n<1 || m<1)
return -1; //出錯
LinkedList<Integer> list = new LinkedList<Integer>();
for(int i=0;i<n;i++)
list.add(i);
int removeIndex=0;
while(list.size()>1){
removeIndex=(removeIndex+m-1)%list.size();
list.remove(removeIndex);
}
return list.getFirst();
}
}
收獲
1.對於下標循環一圈類似的問題,通過%可以很好地實現循環,而不需要我們自己構造循環鏈表;
2.(a%n+b)%n=(a+b)%n
3.盡量學會本題的數學方法,特別是要掌握好數字間映射的方法。
4.公式法中,last=(last+m)% i; //這里是i不是n!!!
