題目
有n個囚犯圍成一圈從1到n編號,并從1開始報數。每當報到k,這個囚犯就會被執行死刑。接著從下一個人開始,剩下的囚犯繼續從1開始報數并重復這個過程。直到所有囚犯的數目加起來小于k。問給定n和k,初始站在什么位置的囚犯能活下來?(k > 1 && k <=n)。
Analysis
對于這道題目,首先我們應該明確當k輸入非法的情況(既k<=1或k>n的情況,這兩種情況下要么囚犯全部無法存活要么全部存活)。在這種情況下程序拋出異常以表示輸入非法。
對于其余的情況,我們可以模擬整個循環報數的過程直到滿足囚犯數目小于k為止。此時剩下的編號即為我們要求的編號。考慮到這種循環遍歷且需要移除某個位置的元素的問題,LinkedList
這種數據結構是比較適合的。因為我們在循環報數的過程中,一定是按順序遍歷的。所以只要利用LinkedList
的next
指針即可。同時考慮到移除當前元素后當前元素的前一個元素的next
指針需要指向當前元素的后面一個元素,使用DoubleLinkedList
能夠大大簡化這一過程的實現。同時考慮到這些囚犯是圍成一個環,所以我們需要讓鏈表的頭尾相連形成一個環狀鏈表。
事實上不使用循環鏈表而使用Queue
的FIFO特性也可以實現這一模擬的過程。
時間復雜度
考慮到我們需要不斷循環整個鏈表,且每報k個數殺掉一個囚犯,且由于使用了雙向鏈表,移除一個元素的時間為O(1)
。因此每移除一個元素需要的時間為k*O(1)=O(k)
。要循環到整個鏈表只剩k-1
個元素,即刪除了n-(k-1)
個元素,那么總時間復雜度為O(k)*(n-k+1)=O(nk)
。
實現如下:
public class JosephProblem {
private static class Node {
Node next;
Node prev;
int val;
Node(int val) {
this.val = val;
}
}
public static List<Integer> josephSurviver(int n, int k) throws IllegalArgumentException {
if (k <= 1 || k > n) {
throw new IllegalArgumentException("k out of boundary!");
}
int size = n;
Node head = new Node(1);
Node curr = head;
for (int i = 1; i < n; i++) {
curr.next = new Node(i + 1);
curr.next.prev = curr;
curr = curr.next;
}
// make the LinkedList a circle
curr.next = head;
head.prev = curr;
curr = head;
while (size >= k) {
// Count off
for (int i = 1; i <= k - 1; i++) {
curr = curr.next;
}
// Remove the kth element
curr.prev.next = curr.next;
curr.next.prev = curr.prev;
size--;
// Start from next person next time.
curr = curr.next;
}
List<Integer> ret = new ArrayList<>();
for (int i = 0; i < size; i++) {
ret.add(curr.val);
curr = curr.next;
}
return ret;
}
}