LeetCode-287 寻找重复数 二分法
287. 寻找重复数
给定一个包含 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
示例 3:输入:nums = [1,1]
输出:1
示例 4:输入:nums = [1,1,2]
输出:1提示:
1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次进阶:
如何证明 nums 中至少存在一个重复的数字?
你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?
对于寻找重复数这种题,常规做法的话如哈希都可以做,但是本题的要求是不修改数组 nums
且只用常量级 O(1)
的额外空间。注意到本题给出了一个常规寻找重复数题目没有的条件:其数字都在 1 到 n 之间(包括 1 和 n)
二分法
记要找的重复数为 target,
c
n
t
(
i
)
cnt(i)
cnt(i) 表示给定数组
n
u
m
s
nums
nums 中不大于
i
i
i 的数字个数。比如
c
n
t
(
3
)
cnt(3)
cnt(3) 就是
n
u
m
s
nums
nums 中1, 2, 3的个数之和,显然
c
n
t
(
i
)
cnt(i)
cnt(i) 是递增的。以数组 {1, 2, 3, 3, 4 ,5}
为例:
i i i | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
c n t ( i ) cnt(i) cnt(i) | 1 | 2 | 4 | 5 | 6 |
根据上面提到的额外的条件,我们可以发现这样一个规律:对于小于 target 的数 i, c n t ( i ) = i cnt(i)=i cnt(i)=i,对于大于等于 target 的数 i, c n t ( i ) > i cnt(i)>i cnt(i)>i 。因此,我们要找的重复数 target,就是最小的使得 c n t ( i ) > i cnt(i)>i cnt(i)>i 的数字 i。可以看到,上例中 target 为3,当 i < 3 i<3 i<3 时, c n t ( i ) = i cnt(i)=i cnt(i)=i,当 i ≥ 3 i\ge3 i≥3 时, c n t ( i ) > i cnt(i)>i cnt(i)>i。
而我们回顾以下常规的二分法,是这样的:对于一个递增(有序)的序列 n u m s nums nums 和 给定值 target,我们要找到最小的使得 n u m s [ i ] > t a r g e t nums[i]>target nums[i]>target 的数字 i。
这就是这个题为什么可以使用二分法,递增序列当然是使用二分法的前提,我们用 c n t ( i ) cnt(i) cnt(i) 来合理地构造;然后根据具体的要找的条件来更新结果就好了。
常规二分法代码(返回索引):
int bin_search(vector<int>& nums, int target) {
int n = nums.size();
int l = 0, r = n-1;
int mid;
while (l<=r) {
mid = (l+r) >> 1;
if (nums[mid] < target)
l = mid + 1;
else if (nums[mid] > target)
r = mid - 1;
else
break;
}
return mid;
}
本题代码(C++):
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int l = 0, r = n-1, ans = -1;
while (l<=r) {
int mid = (l+r) >> 1;
int cnt = 0;
for (int i=0; i<n; ++i) { // 计算cnt(mid),这里的mid就是我们上面公式中的i
cnt += nums[i] <= mid ? 1 : 0;
}
if (cnt<=mid) { // 比较cnt(mid)和mid
l = mid + 1;
}
else { // 若cnt(mid)>mid,则mid有可能是target:所有满足cnt(mid)>mid的值中最小的mid值即为target(即代码中的ans)
ans = mid;
r = mid - 1;
}
}
return ans;
}
};
双指针
我们将数组转换为链表,即将下标 n n n 和数 n u m s [ n ] nums[n] nums[n] 建立一个映射关系 f(n),将 n n n 作为下一个元素 f ( n ) f(n) f(n) 的志向。数组元素的取值范围在 [ 1 , n ] [1,n] [1,n] 之间,而数组长度为 n + 1 n+1 n+1 ,所以按照数组元素取值一定不可能越界。然后利用链表判环的快慢指针方法找到相同元素。
详见:https://leetcode.cn/problems/find-the-duplicate-number/solution/287xun-zhao-zhong-fu-shu-by-kirsche/
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int fast = 0, slow = 0;
while (true) {
fast = nums[nums[fast]];
slow = nums[slow];
if (slow == fast) break;
}
fast = 0;
while (true) {
slow = nums[slow];
fast = nums[fast];
if (slow == fast) return fast;
}
}
};