想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
問題來源:
據說著名猶太歷史學家 Josephus有過以下的故事:在羅馬人佔領喬塔帕特後,39 個猶太人與Josephus及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,於是決定了一個自殺方式,41個人排成一個圓圈,由第1個人開始報數,每報數到第3人該人就必須自殺,然後再由下一個重新報數,直到所有人都自殺身亡爲止。然而Josephus 和他的朋友並不想遵從。首先從一個人開始,越過k-2個人(因爲第一個人已經被越過),並殺掉第k個人。接着,再越過k-1個人,並殺掉第k個人。這個過程沿着圓圈一直進行,直到最終只剩下一個人留下,這個人就可以繼續活着。問題是,給定了和,一開始要站在什麼地方纔能避免被處決?Josephus要他的朋友先假裝遵從,他將朋友與自己安排在第16個與第31個位置,於是逃過了這場死亡遊戲。
一,留下K-1個
41數字太大了,我們就以7爲例,來畫個圖看一下
我們再來看下代碼
1,數組的實現
public static Integer[] solution(int count, int k) {
Integer live[] = new Integer[Math.min(count, k - 1)];
if (count < k) {
int index = 0;
while (index < count) {
live[index++] = index;
}
return live;
}
List<Integer> mList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
mList.add(i + 1);
}
int point = 0;
int number = 0;
while (mList.size() >= k) {
number++;
if (point >= mList.size()) {
point = 0;
}
if (number % k == 0) {
mList.remove(point);
continue;
}
point++;
}
return mList.toArray(live);
}
1, 3-9行表示如果k大於count就直接把所有人的編號都返回即可,不再刪除了。
2, 11-13行生成從1到count的所有值(包含1和count)
3, 16行number統計數量,在第22-25行如果統計的數量是k的倍數就把他移除。其實我們也可以在22行成立的時候讓number重新歸0。這裏使用的是對k求餘也是可以的。
4, 我們就用上面已知的兩組數據測試一下,當count等於41的時候結果是16和31,當count等於7的時候結果是1和4
Util.printObjectArrays(solution(41, 3));
System.out.println("---------------------");
Util.printObjectArrays(solution(7, 3));
運行結果是
16
31
---------------------
1
4
結果完全正確。
數組的刪除會導致後面的元素都會往前移,頻繁的刪除效率肯定不是很高,其實我們還可以使用鏈表。因爲鏈表的刪除不需要移動後面的元素,效率還是比較高的。如果不使用鏈表,我們還可以把數組中刪除的元素用一個負數來填充,這樣也是可以的。我們來看下
2,數組實現的另一種方式
/**
* @param count 總人數
* @param k 每隔幾個人殺掉
* @return
*/
public static Integer[] solution(int count, int k) {
Integer live[] = new Integer[Math.min(count, k - 1)];
if (count < k) {
int index = 0;
while (index < count) {
live[index++] = index;
}
return live;
}
List<Integer> mList = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
mList.add(i + 1);
}
int point = 0;
int number = 0;
int total = count - k + 1;//記錄總共刪除的個數
while (true) {
if (total <= 0)
break;
if (point >= mList.size()) {
point = 0;
}
if (mList.get(point) < 0) {
point++;
continue;
}
number++;
if (number % k == 0) {
mList.set(point, -1);//如果是第k個,就把他變爲負數
total--;
continue;
}
point++;
}
int index = 0;
for (int i = 0; i < mList.size(); i++) {
if (mList.get(i) > 0)
live[index++] = mList.get(i);
}
return live;
}
第35行該刪除的我們沒有刪除,直接把他變爲-1。在29行統計的時候如果爲負數表示已經被刪除了,就直接跳過,執行下一輪循環。第42-45行把最後沒有被刪除的放到數組live中。
3,鏈表實現
一般來說鏈表的斷開要比數組的刪除效率要高一些,因爲數組刪除某個元素之後,它後面的元素都還要往前移。使用鏈表會更簡單一些,我們可以把它想象爲一圈人大家都手牽着手,然後再一個個報數,當報到k的時候就自動退出,退出的時候左右兩邊人的手要牽到一塊重新構成一個新的環,代碼很簡單,我們看下
public static Integer[] solution(int count, int k) {
Integer live[] = new Integer[Math.min(count, k - 1)];
if (count < k) {
int index = 0;
while (index < count) {
live[index++] = index;
}
return live;
}
LinkedList<Integer> mList = new LinkedList<>();
for (int i = 0; i < count; i++) {
mList.addLast(i + 1);
}
int point = 0;
int number = 0;
while (mList.size() >= k) {
number++;
if (point >= mList.size()) {
point = 0;
}
if (number % k == 0) {
mList.remove(point);
continue;
}
point++;
}
return mList.toArray(live);
}
隊列實現:
除了上面說的數組和鏈表以外,我們還可以使用隊列。這個實現起來也非常簡單。我們把所有的元素全部入隊,然後再一個個出隊,出隊的時候記錄出隊的個數,如果不是第k個就讓他重新入隊,如果是第k個就不用了入隊了,然後下一個出隊的再重新從1開始計算。我們還是以7來畫個圖看一下。
我們先來看一下代碼,隊列就是用之前寫的3,常見數據結構-隊列中的雙端隊列。
/**
* @param count 總人數
* @param k 每隔幾個人殺掉
* @return
*/
public static Integer[] solution(int count, int k) {
Integer live[] = new Integer[Math.min(count, k - 1)];
if (count < k) {
int index = 0;
while (index < count) {
live[index++] = index;
}
return live;
}
MyQueue<Integer> queue = new MyQueue<>(count + 1);
for (int i = 0; i < count; i++) {
queue.addLast(i + 1);
}
int number = 1;
while (queue.size() >= k) {
Integer item = queue.removeFirst();
if (number % k == 0) {
number = 1;
continue;
}
queue.addLast(item);
number++;
}
int index = 0;
while (!queue.isEmpty()) {
live[index++] = queue.removeFirst();
}
return live;
}
二,只留下一個
上面我們講的是每到第K個刪除,如果count大於等於k的話,最終會留下k-1個。但對這題還有另一個版本,就是無論多少個,最後只留下1個,就是說如果數量小於k個的時候我們繼續循環刪除,直到留下最後一個的爲止。原理和上面非常類似,只不過當刪除到最後小於K個的時候我們還要繼續循環即可。圖就不再畫了,我們就用最後雙端隊列這種實現來改一下。
1,雙端隊列解決
public static Integer solution(int count, int k) {
MyQueue<Integer> queue = new MyQueue<>(count + 1);
for (int i = 0; i < count; i++) {
queue.addLast(i + 1);
}
int number = 1;
while (queue.size() > 1) {
Integer item = queue.removeFirst();
if (number % k == 0) {
number = 1;
continue;
}
queue.addLast(item);
number++;
}
return queue.removeFirst();
}
2,遞歸解決
我們用f(n,k)表示有n個人,第k個出列,最後列出的人的編號。
那麼f(n-1,k)就表示有n-1個人,第k個出列,最後列出的人的編號。
所以我們可以找到遞歸的公式f(n,k)=f(n-1,k)+k;也就是說n-1個人組成的環相對於n個人組成的環相當於順時針旋轉了k個單位。因爲是環形的,當超出環的大小的時候我們要對它求餘,所以爲了防止越界問題我們要這樣寫f(n,k)=(f(n-1,k)+k)%n。
當f(1,k)的時候就表示剩下最後一個元素了,我們直接返回即可。
我們來看下代碼
public static int solution(int n, int k) {
return helper(n, k) + 1;
}
public static int helper(int n, int m) {
if (n == 1)
return 0;
return (helper(n - 1, m) + m) % n;
}
因爲人的編號是從1開始的,所以這裏要加1。當然我們還可以再來改一下,這樣就不用在加1,就可以直接返回了。
public static int solution(int n, int k) {
if (n == 1)
return n;
return (solution(n - 1, k) + k - 1) % n + 1;
}
3,非遞歸寫法
看明白了上面的遞歸的思路,我們還可以把它改爲非遞歸的寫法
public static int solution(int n, int k) {
int m = 0;
for (int i = 2; i <= n; i++) {
m = (m + k) % i;
}
return m + 1;
}
他的原理是這樣的,從前往後推,如果當n=i的時候,最終留下的是m,那麼當n=i+1的時候,最終留下的就是m+k,考慮到m+k可能大於環的長度,所以要對m+k進行求餘,結果就是(m+k)%i,一直循環到i等於n就是最終結果。
總結:
這題只要是學過編程的大多數應該都聽過,無論是使用數組,鏈表,還是隊列都很容易解決,具有一定的代表性,希望大家能夠熟練掌握。