给定一个长度为n+1的只读数组,里面所有的元素都位于[1, n],所以很明显有重复元素,题目假设只有一个重复数字(但可以重复多次),求这个重复数字。要求空间复杂度为常数且时间复杂度小于O(n^2)。
题目说的时间复杂度要小于O(n^2),那么我们可以想到比这个复杂度小一个量级的常见复杂度O(nlogn),由此可想到二分法。 由于重复值肯定是在[1, n]之间,所以我们可以首先得到这个范围的中点mid,然后遍历整个数组,统计所有小于等于mid的数的个数count:
- 如果
count <= mid
,则说明重复值一定是大于mid(可以自己举例子理解),应进入右半部分继续二分; - 否则,重复值不大于mid。
此时时间复杂度就是O(nlogn)
由于要求常数空间复杂度,所以就不能用hash什么的;然后要求时间复杂度小于O(n^2),暴力计数也不行。既然直接计数不行,那么我们可以考虑位操作: 计算32位中每一位当中1出现的次数。具体来讲,对于第i位(0 <= i <= 31),我们用count1代表整个nums数组中所有元素第i位是1的个数, 用count2代表0~n这些数中第i位是1的个数。若 count1 > count2,说明最终所求的重复元素在第i位是1,否则是0。这样我们就可以通过分别计算32位1出现的次数来得到结果。
时间复杂度为O(n)
注意此思路work的前提是数组中重复元素只有一个!
此题还有一个十分巧妙的方法[可参考此处]。此时要把nums数组看成一个链表:nums[i] = j
表示链表中结点i的next结点为j,例如数组
index : 0 1 2 3 4
nums[i]: 3 1 3 4 2
表示的链表为
0 -> 3 -> 4 -> 2
^ |
|_________|
可以看到这个链表是有环的,为什么有环呢,就是因为数组nums中存在重复元素3。仔细分析我们可以发现,链表中环的开始结点即为重复元素, 即题目转变为求一个带环链表中环的开始结点,这其实就是142. Linked List Cycle II,可参见这题我的题解,这里直接给出代码。
注意此思路work的前提是数组中元素不可能为0,即0一定不在环内!
本题要求数组nums是只读的,即不能改变nums,如果可改变nums,那么此题还有其他时间复杂度O(n)空间复杂度O(1)的解法(剑指offer第二版面试题3)。
由于所有元素都在区间[1, n]之内,所以如果没有重复的数字,那当数组排序后数字m会排在第m位(即下标是m-1)。 所以我们可以重排这个数组。用while循环从头到尾扫描这个数组,当扫描到nums[i]时,
- 如果
i == nums[i] - 1
,则这个数字已经在它该在的位置,i加1然后进入下一循环即可; - 否则,如果
nums[i] == nums[nums[i]-1]
,即nums[i]
出现了两次,找到了重复值,返回即可。 - 否则,即
nums[i] != nums[nums[i]-1]
,应该交换nums[i]
和nums[nums[i]-1]
,进入下一循环(i不加1)。
因为调用swap的次数是线性的,所以时间复杂度还是O(n),而没有引入额外的数组,所以空间复杂度是O(1)。
由于不能有额外的数组或者数据结构如hashmap来标记某个数字之前是否出现过,但是注意数组里数字的范围保证在[1, n]之间,所以可以利用现有数组设置标志,当一个数字被访问过后,可以将它索引到的数减n(也可加n,只是溢出风险大一些),之后如果发现当前元素索引到的数小于等于0就说明当前元素之前出现过一次了。此方法相对于思路四的优点是算法结束前可以将数组恢复原样。
- 重复数字可能有多个(如剑指offer面试题3),此时思路二不再适用;
- 元素范围为[0, n-1](如剑指offer面试题3_1),此时思路三不再适用。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size() - 1;
int low = 1, high = n;
while(low < high){
int mid = low + (high - low) / 2;
int count = 0;
for(auto &num: nums)
if(num <= mid) count++;
if(count <= mid) low = mid + 1;
else high = mid;
}
return low;
}
};
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int res = 0, mask = 1, n = nums.size() - 1;
for(int i = 0; i < 32; i++){
int count1 = 0, count2 = 0;
for(int j = 0; j <= n; j++){
if(mask & nums[j]) count1++;
if(mask & j) count2++;
}
if(count1 > count2) res += mask;
if(i < 31) mask <<= 1;
}
return res;
}
};
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
while(true){
slow = nums[slow];
fast = nums[nums[fast]];
if(slow == fast) break;
}
int p = 0;
while(true){
if(p == slow) break;
slow = nums[slow];
p = nums[p];
}
return slow;
}
};
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size() - 1;
int i = 0;
while(i <= n){
if(i == nums[i] - 1) i++;
else if(nums[i] != nums[nums[i]-1]) swap(nums[i], nums[nums[i]-1]);
else return nums[i];
}
return 0;
}
};
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size() - 1;
for(int i = 0; i <= n; i++){
int tmp = nums[i];
if(tmp <= 0) tmp += n; // 当前元素实际值
// tmp索引到的数小于等于0说明tmp之前出现过
if(nums[tmp] <= 0) return tmp;
nums[tmp] -= n;
}
// 算法结束前可以再遍历一遍数组将其恢复原样
return 0;
}
};