287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
1 不能更改原数组(假设数组是只读的)。
2 只能使用额外的 O(1) 的空间。
3 时间复杂度小于 O(n2) 。
4 数组中只有一个重复的数字,但它可能不止重复出现一次。
思路一:二分法:
参考:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/er-fen-fa-si-lu-ji-dai-ma-python-by-liweiwei1419/
因为题目有几个要求,不能修改原数组,所以不能使用排序,然后统计相邻的元素是否相同;空间复杂度只能为O(1), 所以不能借助计数排序或者 HashMap统计元素个数。所以使用二分法来解决这个问题。
让左右指针分别等于可能的最大值和可能的最小值,注意这个左右指针存储的不是下标,而是元素值;随后取左右元素的一半,遍历nums 数组,统计小于等于该中值的元素个数,如果元素个数大于mid, 说明前半段元素存在重复,说明[left, mid] 中存在重复,因为如果不存在重复的话,[left, mid]中元素最多为 (mid - left + 1), 肯定小于等于mid, 应该让rihgt = mid; 否则说明重复的元素在右半段,即重复元素存在于 [mid+1, right], 所以应该让left = mid + 1
1 class Solution {
2 public int findDuplicate(int[] nums) {
3
4 // 二分查找小于等于中值的元素个数
5
6 int left = 1, right = nums.length -1;
7 while(left < right){
8 int mid = (left + right) >>> 1; // 取左右的中值
9 int cnt = 0;
10 // 遍历数组,求出小于等于mid的元素的个数
11 for(int num : nums){
12 if(num <= mid){
13 cnt++;
14 }
15 }
16
17 // 如果cnt严格大于mid, 说明重复元素一定在[left, mid]之间
18 if(cnt > mid){
19 right = mid;
20 }else{
21 left = mid + 1; // 否则在[mid+1, right]之间
22 }
23
24 }
25 return left;
26 }
27 }
leetcode 执行用时:3 ms > 59.94%, 内存消耗:38.6 MB > 90.11%
复杂度分析:
时间复杂度:二分法每次把值的范围缩小一半,所以二分法的时间复杂度为O(logn), 另外每次二分的过程中需要遍历整个nums数组来统计小于等于 mid 元素的个数,这里的花费是O(n), 所以时间复杂度为O(nlogn)
空间复杂度:只需要常量级别的变量空间,所以空间复杂度为O(1)
思路二: 快慢指针
参考:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/xun-zhao-zhong-fu-shu-by-leetcode-solution/
我们对 nums[] 数组建图,每个位置 ii 连一条 i→nums[i] 的边。由于存在的重复的数字 target,因此 target 这个位置一定有起码两条指向它的边,因此整张图一定存在环,且我们要找到的 target 就是这个环的入口,那么整个问题就等价于 力扣142.环形链表II & 剑指offer 55. 链表中环的入口结点。
证明为什么快慢指针可以求得入环结点可以看力扣142.环形链表II & 剑指offer 55. 链表中环的入口结点
1 class Solution {
2 public int findDuplicate(int[] nums) {
3 int slow = 0, fast = 0;
4 do{
5 slow = nums[slow];
6 fast = nums[nums[fast]];
7 }while(fast != slow);
8
9 fast = 0; // 快指针拉回到起点
10 while(slow != fast){
11 slow = nums[slow];
12 fast = nums[fast];
13 }
14 return slow;
15 }
16 }