算法 - 找重復數字 (抽屜,鏈表環)
Find the Duplicate Number
Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.
Note: You must not modify the array (assume the array is read only). You must use only constant, O(1) extra space. Your runtime complexity should be less than O(n2). There is only one duplicate number in the array, but it could be repeated more than once.
哈希表法
復雜度
時間 O(N) 空間 O(N)
思路
遍歷數組時,用一個集合記錄已經遍歷過的數,如果集合中已經有了說明是重復。但這樣要空間,不符合。
暴力法
復雜度
時間 O(N^2) 空間 O(1)
思路
如果不用空間的話,最直接的方法就是選擇一個數,然後再遍歷整個數組看是否有跟這個數相同的數就行了。
排序法
復雜度
時間 O(NlogN) 空間 O(1)
思路
更有效的方法是對數組排序,這樣遍歷時遇到前後相同的數便是重復,但這樣要修改原數組,不符合要求。
二分法
復雜度
時間 O(NlogN) 空間 O(1)
思路
實際上,我們可以根據抽屜原理簡化剛才的暴力法。我們不一定要依次選擇數,然後看是否有這個數的重復數,我們可以用二分法先選取n/2,按照抽屜原理,整個數組中如果小於等於n/2的數的數量大於n/2,說明1到n/2這個區間是肯定有重復數字的。比如6個抽屜,如果有7個襪子要放到抽屜裏,那肯定有一個抽屜至少兩個襪子。這裏抽屜就是1到n/2的每一個數,而襪子就是整個數組中小於等於n/2的那些數。這樣我們就能知道下次選擇的數的範圍,如果1到n/2區間內肯定有重復數字,則下次在1到n/2範圍內找,否則在n/2到n範圍內找。下次找的時候,還是找一半。
註意
-
我們比較的
mid
而不是nums[mid]
-
因為mid是下標,所以判斷式應為
cnt > mid
,最後返回min
代碼
public class Solution {
public int findDuplicate(int[] nums) {
int min = 0, max = nums.length - 1;
while(min <= max){
// 找到中間那個數
int mid = min + (max - min) / 2;
int cnt = 0;
// 計算總數組中有多少個數小於等於中間數
for(int i = 0; i < nums.length; i++){
if(nums[i] <= mid){
cnt++;
}
}
// 如果小於等於中間數的數量大於中間數,說明前半部分必有重復
if(cnt > mid){
max = mid - 1;
// 否則後半部分必有重復
} else {
min = mid + 1;
}
}
return min;
}
}
映射找環法
復雜度
時間 O(N) 空間 O(1)
思路
假設數組中沒有重復,那我們可以做到這麽一點,就是將數組的下標和1到n每一個數一對一的映射起來。比如數組是213
,則映射關系為0->2, 1->1, 2->3
。假設這個一對一映射關系是一個函數f(n),其中n是下標,f(n)是映射到的數。如果我們從下標為0出發,根據這個函數計算出一個值,以這個值為新的下標,再用這個函數計算,以此類推,直到下標超界。實際上可以產生一個類似鏈表一樣的序列。比如在這個例子中有兩個下標的序列,0->2->3
。
但如果有重復的話,這中間就會產生多對一的映射,比如數組2131
,則映射關系為0->2, {1,3}->1, 2->3
。這樣,我們推演的序列就一定會有環路了,這裏下標的序列是0->2->3->1->1->1->1->...
,而環的起點就是重復的數。
所以該題實際上就是找環路起點的題,和Linked List Cycle II一樣。我們先用快慢兩個下標都從0開始,快下標每輪映射兩次,慢下標每輪映射一次,直到兩個下標再次相同。這時候保持慢下標位置不變,再用一個新的下標從0開始,這兩個下標都繼續每輪映射一次,當這兩個下標相遇時,就是環的起點,也就是重復的數。對這個找環起點算法不懂的,請參考Floyd‘s Algorithm。
註意
第一次找快慢指針相遇用do-while循環
代碼
public class Solution {
public int findDuplicate(int[] nums) {
int slow = 0;
int fast = 0;
// 找到快慢指針相遇的地方
do{
slow = nums[slow];
fast = nums[nums[fast]];
} while(slow != fast);
int find = 0;
// 用一個新指針從頭開始,直到和慢指針相遇
while(find != slow){
slow = nums[slow];
find = nums[find];
}
return find;
}
}
算法 - 找重復數字 (抽屜,鏈表環)