題目描述
給定一個包含 n + 1 個整數的數組 nums,其數字都在 1 到 n 之間(包括 1 和 n),可知至少存在一個重複的整數。假設只有一個重複的整數,找出這個重複的數。
輸入: [1,3,4,2,2]
輸出: 2
說明:
不能更改原數組(假設數組是隻讀的)。
只能使用額外的 O(1) 的空間。
時間複雜度小於 O(n2) 。
數組中只有一個重複的數字,但它可能不止重複出現一次。
初步探索
首先,最簡單的一種思路是兩兩比較是否相等,如果相等就輸出。然而,由於O(n2) 的限制,不能用這種算法。
其次,又想到可以利用這些數字都是1到n中,可以將他們相加,然後通過和1到n的總和比較得出相差多少。然而,這個算法也又缺陷,因爲由於數組中重複的數字可能重複多次,難以做到準確判斷。
最後,由於時間複雜度的限制,想到可能可以使用二分搜索法。但是不能進行排序,因爲原數組不可改變,也不能使用O(n)的額外空間。
綜合方法
如何將這些思考綜合起來呢?
我想到即使沒有排序,由於數字的範圍是固定的,可以使用一種變相的二分法。模糊的思路是,如果這個數組是恰好1,2,3…n,那麼這個數組可以看成是"平衡"的,因爲每個數字都出現了一次,其中位數是在(1+n)/2的地方。然而,如果某一個數字出現了兩次,那麼會有兩種情況
- 這個數字小於中位數,那麼我們將數組排列後,新的中位數將會小於原本的中位數
- 這個數字大於中位數,則同理,新的中位數將大於原本的中位數
- 這個數字等於中位數,則中位數重複
是不是有點像二分法?這裏,我們並不需要排序數組,只需要判斷理論和實際的中位數的位置就可以逐步縮小搜索範圍了。
- 如果新的中位數小於原本中位數,則重複數字小於原本中位數
- 同理可得新的中位數大於原本中位數的情況
- 中位數重複,那麼重複的數字就是是中位數
如何找中位數的位置呢?我的方法是
- 找到理論中位數,即左邊界l和右邊界r的平均, mid=(l+r)/2
- 遍歷數組,統計小於中位數的數字個數 smaller,大於中位數的數字個數 bigger,等於中位數的數字個數equal
- 如果equal>2,說明mid就是我們要找的數字
- 如果smaller > bigger, 則將搜索範圍縮小到 l到mid,即將r賦值爲floor(mid),smaller<bigger同理
- 重複1-4
細節
注意,在數學定義中,如果有偶數個數字,中位數則是中間兩個數字的平均數。在這道題中,也必須延用這樣的定義,而不能簡單把mid定義爲int,使其向下取整。與一般二分搜索不同的是,我們需要考慮大於和小於的數字個數。如果數列有偶數個數字,向下取整後的中位數會使得一個已經平衡的數列變得不平衡,因爲原本小於中位數的數字被算作等於了。所以,最簡單的解決方法就是使用double定義mid。當然,這個中位數的0.5在邊界時是沒有意義的,故在縮小搜索範圍時可以用floor和ceiling函數變爲整數。
代碼
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n=nums.size()-1;
int l=1;// left boundary
int r=n;// right boundary: both are representing numbers but not indexes
int ans=0;//solution
while(1){
int bigger=0;//the count of numbers bigger than the median
int smaller=0;// the count of numbers smaller than the median
int equal=0;// the count of numbers equal to median
double mid=(l+r)*0.5;//calculate median
int i=0;
for(i=0;i<=n;i++)
{
if(nums[i]>mid&&nums[i]<=r) bigger++;
if(nums[i]<mid&&nums[i]>=l) smaller++;
if(nums[i]==mid) equal++;
}
if(equal>=2){
ans=mid;// answer found!
break;
}
if(bigger>smaller) l=ceil(mid);// modify the boundary
if(bigger<=smaller) r=floor(mid);
}
return ans;
}
};