题目描述
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例1:
输入: nums = [1,3,4,2,2]
输出: 2
示例2:
输入: nums = [3,1,3,4,2]
输出: 3
提示
- 1 < = n < = 1 0 5 1 <= n <= 10^5 1<=n<=105
- n u m s . l e n g t h = = n + 1 nums.length == n + 1 nums.length==n+1
- 1 < = n u m s [ i ] < = n 1 <= nums[i] <= n 1<=nums[i]<=n
- n u m s 中只有一个整数出现两次或多次,其余整数均只出现一次 nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次 nums中只有一个整数出现两次或多次,其余整数均只出现一次
方法一:二分查找
解题思路
我们用 c n t [ i ] cnt[i] cnt[i] 表示 n u m s nums nums 数组中元素小于等于 i 的个数。假设重复的数是 target,那么 [ 1 , t a r g e t − 1 ] [1,target-1] [1,target−1] 范围内的所有数都满足 c n t [ i ] < = i cnt[i]<=i cnt[i]<=i,而 [ t a r g e t , n ] [target,n] [target,n] 范围内的所有数都满足 c n t [ i ] > i cnt[i]>i cnt[i]>i。因此,具有单调性,可以利用二分查找来解题。
以 n u m s = [ 1 , 3 , 4 , 2 , 2 ] nums = [1,3,4,2,2] nums=[1,3,4,2,2] 为例,我们可以列出每个数字的 c n t cnt cnt 值:
nums | 1 | 2 | 3 | 4 |
---|---|---|---|---|
cnt | 1 | 3 | 4 | 5 |
由此可以验证我们上面的推断,当 t a r g e t = 2 target = 2 target=2 时, [ 1 , 1 ] [1,1] [1,1] 范围内的所有数满足 c n t [ i ] < = i cnt[i]<=i cnt[i]<=i,而 [ 2 , 4 ] [2,4] [2,4] 范围内的所有数满足 c n t [ i ] > i cnt[i]>i cnt[i]>i。
现在可以得出结论:
- 当 1 < = i < = t a r g e t − 1 1 <= i <= target - 1 1<=i<=target−1时, c n t [ i ] < = i cnt[i] <= i cnt[i]<=i;
- 当 t a r g e t < = i < = n target<=i<=n target<=i<=n时, c n t [ i ] > i cnt[i]>i cnt[i]>i。
代码
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int l = 1, r = n - 1, mid;
while(l < r)
{
mid = (l + r) >> 1;
int cnt = 0;
for(int i = 0; i < n; i++)
if(nums[i] <= mid)
cnt++;
if(cnt > mid)
r = mid;
else
l = mid + 1;
}
return l;
}
};
复杂度分析
- 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)。二分查找的时间复杂度为 O ( l o g n ) O(logn) O(logn),每一轮二分查找都需要遍历数组的时间复杂度为 O ( n ) O(n) O(n),因此整体时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
方法二:快慢指针
解题思路
假设有这样一个样例子:[1,2,3,4,5,6,7,8,9,5]。如果我们按照上面的循环下去就会得到这样一个路径: 1 2 3 4 5 [6 7 8 9] [6 7 8 9] [6 7 8 9] . . .这样就有了一个环,也就是6 7 8 9。指针会一直在环中循环的前进。
这时我们设置两个一快(fast)一慢(slow)两个指针,一个每次走两步,一个每次走一步,这样让他们一直走下去,直到他们在重复的序列中相遇。
如上图,slow 和 fast 会在环中相遇,先假设一些量:起点到环的入口长度为m,环的周长为 c,在 fast 和 slow 相遇时 slow 走了 n 步。则 fast 走了 2n 步,fast 比 slow 多走了 n 步,而这n步全用在了在环里循环(
n
%
c
=
=
0
n\%c==0
n%c==0)。
当 fast 和 slow 相遇之后,我们设置第三个指针 finder,它从起点开始和 slow (在 fast 和 slow 相遇处)同步前进,当 finder 和 slow 相遇时,就是在环的入口处相遇,也就是重复的那个数字相遇。
为什么 finder 和 slow 相遇在入口
fast 和 slow 相遇时,slow 在环中行进的距离是 n - m,其中
n
%
c
=
=
0
n\%c == 0
n%c==0。这时我们再让 slow 前进 m 步——也就是在环中走了 n 步了。而
n
%
c
=
=
0
n\%c == 0
n%c==0 即 slow 在环里面走的距离是环的周长的整数倍,就回到了环的入口了,而入口就是重复的数字。
我们不知道起点到入口的长度m,所以弄个 finder 和 slow 一起走,他们必定会在入口处相遇。
代码
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];
while(slow != fast)
{
slow = nums[slow];
fast = nums[nums[fast]];
}
int finder = 0;
while(finder != slow)
{
finder = nums[finder];
slow = nums[slow];
}
return finder;
}
};
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n)。「Floyd 判圈算法」时间复杂度为线性的时间复杂度。
- 空间复杂度: O ( 1 ) O(1) O(1)。