给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间,包括 1 和 n ,可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
- 不能更改原数组(假设数组是只读的)。
- 只能使用额外的 O(1) 的空间。
- 时间复杂度小于 O(n2) 。
- 数组中只有一个重复的数字,但它可能不止重复出现一次。
这道题如果没有说明中的限制条件,允许申请O(n)的辅助空间的话,可以用数组代替哈希表来做,但是题目只能使用O(1)的辅助空间,这道题才变得有意思起来。基于暴力法和数组哈希表法这里不再赘述,提供两种新的解题思路,二分法和循环链表法
二分法:
很多博客直接上来就是代码,让人云里雾里,解释的不也到位,需要阅读者花费很长时间来理解和吸收。这里直接以举例的方式来讲解。比如原数组a为(重复数字为2)
[1,2,2,3,4,5]
一共n+1个数字(n=5),数字范围为1-n及1-5,根据抽屉原理,在数字1-5中有6个数字,则至少有一个数字出现了两次以上。我们初始化low=1(数字的范围下限),high=5(数字的范围上限),mid=(high+low)/2=3,则mid把原数组分为1-3(包含3)和大于3的部分,如果原数组的数字在1-3的出现的次数cnt大于mid(简单理解为数字1-3最多只能包含3个数字,如果包含的数字个数大于3个,那么一定会出现重复的数字,及重复的数字一定在1-3的范围内),所以每次二分,在每个二分循环内都遍历一次数组,时间复杂度为O(nlogn),空间复杂度为O(1)。
代码如下:
int findDuplicate(vector<int>& nums) {
//数字下限
int low = 1;
//数字上限
int high = nums.size() - 1;
while (low < high) {
int cnt = 0;
int mid = (low + high) / 2;
for (int i = 0; i < nums.size(); i++) {
//统计小于等于mid的个数
if (nums[i] <= mid) {
cnt++;
}
}
//如果大于mid则在数字在小的那部分,及low-mid
if (cnt > mid) {
high = mid;
}
//否则,在大的那部分mid+1-high
else {
low = mid + 1;
}
}
return low;
}
循环链表法:
这种方法非常巧妙,中心思想是如果数组中存在重复数字,那对数组a而言:
i->a[i]->a[a[i]]->a[a[a[i]]]
的访问方式最后一定会陷入循环,且循环的数就是重复的数,以下举例说明:
对于不重复的数组:
[1,2,6,3,4,5,8]
我们以下方式生成链表:下标(0)->下标对应的值(1)->下标对应的值[下标对应的值](2)。。。的方式来遍历数组,可以得到如下链表:
0->1->2->6->8->nullptr
不会出现循环,如果对于重复数组:
[1,2,2,3,4,5]
构建的链表为:
0->1->2->2->2->2->......(为什么不是第一个2,因为循环是从第二个2才开始循环,第一个2只是刚好下标为1的数为2)
节点2开始循环,重复的数字也是2,所以思路转化为寻找带环链表的第一个环节点,通过快慢指针来做,之前leetcode已经分析过,点击这里,这里不再赘述。
int findDuplicate(vector<int>& nums) {
int n = nums.size();
if (n > 0) {
//访问下标0的值
int slow = nums[0];
//“下标0的值”作为下标访问
int fast = nums[nums[0]];
while (slow != fast) {
slow = nums[slow];
fast = nums[nums[fast]];
}
fast = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
return -1;
}
同时,剑指offer也有同样的题,但是题目有些许出入:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
注意:数字的范围变成了0~n-1,且数组可修改,之前讨论的二分法可以,循环链表法不行。因为循环链表从第0个元素开始且数组中的值从1开始,可以保证从第一个元素开始不会再回到第一个元素,不像这道题(比如[1,0],该数组会形成:第一个元素->第二个元素->第一个元素......的死循环,但是其实并没有重复元素)。
所以这道题的做法是每次把固定当前元素i,把当前元素的值array[i]交换到对应的下标index是array[i]的地方,相当于归位操作:0对应a[0],1对应a[1],如果a[2]=7,那么就把7和a[7]交换 (swap(a[7],a[2])),让a[7]对应正确的下标,直到找到a[i]==a[j],那么a[i]和a[j]就是重复的数据。
参考代码:
class Solution {
public:
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
bool duplicate(int numbers[], int length, int* duplication) {
for(int i=0;i<length;i++){
while(i!=numbers[i]){
if(numbers[i]==numbers[numbers[i]]) {
*duplication=numbers[i];
return true;
}
swap(numbers[i],numbers[numbers[i]]);
}
}
return false;
}
};