可采用hash、暴力、排序、映射、二分等多种方法。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
方法一、哈希表法
(1)复杂度:时间 O(N) 空间 O(N)
(2)思路:遍历数组时,用一个集合记录已经遍历过的数,如果集合中已经有了说明是重复。但这样要空间,不符合。
方法二、暴力法
(1)复杂度:时间 O(N^2) 空间 O(1)
(2)思路:如果不用空间的话,最直接的方法就是选择一个数,然后再遍历整个数组看是否有跟这个数相同的数就行了。
方法三、排序法
(1)复杂度:时间 O(NlogN) 空间 O(1)
(2)思路:更有效的方法是对数组排序,这样遍历时遇到前后相同的数便是重复,但这样要修改原数组,不符合要求。
方法四、映射找还法
(1)复杂度:时间 O(N) 空间 O(1)
(2)思想:类似于Linked List Cycle II一样寻找单链表环的起始点
假设数组中没有重复,那我们可以做到这么一点,就是将数组的下标和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
。
int findDuplicate(vector<int>& nums) {
//像寻找单链表的环入口点的思路一样,设置一块一慢的指针,由于数组中存在重复节点,则可通过下标来联想成链表
int len = nums.size();
if(len == 1 || len == 0)
return -1;
int slow = nums[0];
int fast = nums[slow];
//以下为求两个指针第一次相遇的点
while(slow != fast)
{
slow = nums[slow];
fast = nums[nums[fast]];
}
//以下求重复元素的节点,即重复元素的入口节点
fast = 0;
while(fast != slow)
{
fast = nums[fast];
slow = nums[slow];
}
return slow;
}
方法五、二分法
(1)复杂度:时间 O(NlogN) 空间 O(1)
(2)思路:诸如:二分查找,思想利用下标来实现,诸如:1...10的数中,如果小于5的数目大于5,那么表明重复的数字一定在小于5的数目中,妙哉. 实际上,我们可以根据抽屉原理简化刚才的暴力法。我们不一定要依次选择数,然后看是否有这个数的重复数,我们可以用二分法先选取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范围内找。下次找的时候,还是找一半。
int findDuplicate(vector<int>& nums) {
//二分查找,思想利用下标来实现,诸如:1...10的数中,如果小于5的数目大于5,那么表明重复的数字一定在小于5的数目中,妙哉
int len = nums.size();
if(len == 0 || len == 1)
return -1;
int low = 0;
int high = len - 1;
while(low <= high)
{
int mid = low + (high - low) / 2;
int count = 0;
for(int i = 0; i < len; i++)
if(nums[i] <= mid)
count++;
if(count > mid)
high = mid - 1;
else
low = mid + 1;
}
return low;
}