LeetCode_Array_287. Find the Duplicate Number 寻找重复数(C++/Java)【二分法、快慢指针】

目录

一,题目描述

英文描述

中文描述

二,解题思路

方法一:二分法

1)核心算法

2)复杂度证明

方法二:快慢指针

1)核心算法

2)复杂度证明

三,AC代码

方法一:二分法

C++

Java

方法二:快慢指针

C++

Java

四,解题过程

方法一:二分法

第一搏

第二搏

方法二:快慢指针

第一搏


一,题目描述

原题链接287. 寻找重复数

英文描述

Given an array of integers nums containing n + 1 integers where each integer is in the range [1, n] inclusive.

There is only one duplicate number in nums, return this duplicate number.

Follow-ups:

How can we prove that at least one duplicate number must exist in nums? 
Can you solve the problem without modifying the array nums?
Can you solve the problem using only constant, O(1) extra space?
Can you solve the problem with runtime complexity less than O(n2)?
 

Example 1:

Input: nums = [1,3,4,2,2]
Output: 2

Example 2:

Input: nums = [3,1,3,4,2]
Output: 3

Example 3:

Input: nums = [1,1]
Output: 1

Example 4:

Input: nums = [1,1,2]
Output: 1
 

Constraints:

2 <= n <= 3 * 104
nums.length == n + 1
1 <= nums[i] <= n
All the integers in nums appear only once except for precisely one integer which appears two or more times.

中文描述

给定一个包含 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) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。

二,解题思路

方法一:二分法

1)核心算法

题目中给定了条件:

  • 所有数字在[1,n-1]之内,n为数组长度;
  • 只有一个重复数字;

所以最终答案ans满足两个条件:

  1. 数组中出现小于ans的数字个数,小于ans本身;(多举几个数组试试就知道了)
  2. ans是满足条件1的最后一个数字;

接下来就可以根据上面ans的条件进行二分查找了:

  • 根据左右边界确定mid,查找数组中小于mid的数字个数cnt;(小于等于的话需要调整边界更新条件、ans更新位置)
  • cnt<mid:left = mid + 1,ans = mid(记录可能是答案的数字)
  • cnt>=mid:right = mid - 1;

2)复杂度证明

时间复杂度: O(nlogn), 其中n为nums[]数组的长度。二分查找最多需要二分O(logn) 次,每次判断的时候需要O(n)遍历nums[]数组求解小于mid的数的个数,因此总时间复杂度为O(n log n)。

空间复杂度: O(1)。 我们只需要常数空间存放若干变量。

方法二:快慢指针

1)核心算法

非常巧妙的借助了数学思路:

我们对nums[]数组建图,每个位置i连一条i→nums[i]的边。由于存在的重复的数字target, 因此target这个位置一定有起码两条指向它的边, 因此整张图一定存在环,且我们要找到的target 就是这个环的入口

里面有几个关键问题:

1,为什么target这个位置一定有起码两条指向它的边?

题目条件是,只有一个重复的数,所以至少有两个位置i、j,使得nums[i] == nums[j] == target,即i → target、j → target。


2,为什么一定存在环?

正向推导有点麻烦,可以考虑什么条件下不存在环。

首先,由于数组中的数字都是[1,n],数组大小为n+1,所以从位置0出发构建的图一定不会再回到0;

从0出发路径上的节点有两个选择:

  • 选择路径的节点作为下个节点;(这样就构成了环)
  • 选择路径的节点作为下个节点;(这样可以不断扩展从0出发的联通图)

(这就是为什么选择从0开始)

要想不构成环,有两点需要注意:

  • 从0出发构成的连通图中,位置i 和 nums[i]不相同(若相同的话,i→nums[i]自身 ,且由于是联通图,所以另外至少一条边也连着nums[i],这样就构成了环);
  • 假设该联通图中有6个点,每个点都有对应的一条边,那么共有6条边,然而6个点构成的连线如果不存在环的话,最多只能有5条边,那多出来的那条边指向哪里了呢?当然是指向我们的答案了o(* ̄▽ ̄*)ブ

因此条件不成立,图中一定有环!


3,为什么target就是环的入口呢?

从0出发的联通图中必定有环,而且没有一个节点指向0,那么看图应该很清楚了吧。


4,怎么找环的入口?

我们先设置慢指针slow和快指针fast,慢指针每次走一步,快指针每次走两步。根据(Floyd判圈算法)两指针在有环的情况下一定会相遇, 此时我们再样slow放置起点0,两个指针每次同时移动一步,相遇点就是答案。(图源 @力扣)

很优雅的数学证明(☆▽☆)

2)复杂度证明

时间复杂度:O(n)。「Floyd 判圈算法」时间复杂度为线性的时间复杂度。

空间复杂度:O(1)。我们只需要常数空间存放若干变量。

三,AC代码

方法一:二分法

C++

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size() - 1, ans = 0;
        while(left <= right){   // !!!这里需要改为等于 否则ans不会更新
            int mid = (left + right) >> 1;
            int smallNum = 0;
            for(int i = 0; i < nums.size(); i++){
                smallNum += (nums[i] < mid) ? 1 : 0;
            }
            if(smallNum < mid){
                left = mid + 1;
                ans = mid;
            }
            else right = mid - 1;
        }
        return ans;
    }
};

Java

class Solution {
    public int findDuplicate(int[] nums) {
        int left = 1, right = nums.length - 1, ans = 0;
        while(left <= right) {
            int mid = (left + right) >> 1;
            int cnt = 0;
            for(int x : nums){
                cnt += (x < mid) ? 1 : 0;
            }
            if(cnt < mid) {
                left = mid + 1;
                ans = mid;
            } else {
                right = mid - 1;
            }
        }
        return ans;
    }
}

方法二:快慢指针

C++

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int fast = 0, slow = 0;
        // 阶段一:寻找相遇点
        do{
            fast = nums[nums[fast]];// 双重索引相当于走两步
            slow = nums[slow];
        } while(slow != fast);
        slow = 0;
        // 阶段二:寻找环入口
        while(nums[slow] != nums[fast]){
            slow = nums[slow];
            fast = nums[fast];
        }
        return nums[fast];
    }
};

Java

class Solution {
    public int findDuplicate(int[] nums) {
        int slow = 0, fast = 0;
        do{
            slow = nums[slow];
            fast = nums[nums[fast]];
        } while (slow != fast);
        slow = 0;
        while(nums[slow] != nums[fast]) {
            slow = nums[slow];
            fast = nums[fast];
        }
        return nums[slow];
    }
}

四,解题过程

参考:@力扣官方题解【寻找重复数】

按照题目要求,时间O(N^2)以内,空间O(1),原数组只读,着实想不出办法,果断向解题区大佬求助。◑﹏◐

看懂大致思路后自己敲了一遍,并且对比高效题解进行一步步的修改,记录如下:(只测试了二分法和快慢指针,二进制那个方法操作太高端,没敢看)

方法一:二分法

第一搏

由于是寻找满足条件1(数组中出现小于ans的数字个数,小于ans本身)的最后一个数字,也就是说即使mid为ans时,如果不能确定mid为最后一个符合此条件的数字,盲目的left = mid + 1 / right = mid - 1,那么很有可能在边界划分时,与答案失之交臂。

所以加上了repeatNum来记录数字的重复次数,并优先判断其是否大于1 ,以此决定算法是否终止。

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size() - 1;
        while(left < right){
            int mid = (left + right) >> 1;
            int smallNum = 0, repeatNum = 0;
            for(int i = 0; i < nums.size(); i++){
                smallNum += (nums[i] < mid) ? 1 : 0;
                repeatNum += (nums[i] == mid) ? 1 : 0;
            }
            if(repeatNum > 1) return mid;
            if(smallNum < mid) left = mid + 1;
            else right = mid - 1;
        }
        return left;
    }
};

第二搏

其实上面的做法是不断打补丁,挤出来的,确实不优雅。

欣赏了官方题解后才恍然大悟。o(* ̄▽ ̄*)ブ

left = mid + 1 / right = mid - 1即可,但是需要将可能的mid值作为ans暂存起来,这样就不会错过正确答案了。

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size() - 1, ans = 0;
        while(left <= right){   // !!!这里需要改为等于 否则ans不会更新
            int mid = (left + right) >> 1;
            int smallNum = 0;
            for(int i = 0; i < nums.size(); i++){
                smallNum += (nums[i] < mid) ? 1 : 0;
            }
            if(smallNum < mid){
                left = mid + 1;
                ans = mid;
            }
            else right = mid - 1;
        }
        return ans;
    }
};

方法二:快慢指针

第一搏

知道算法后,编程就很简单了。

猜你喜欢

转载自blog.csdn.net/qq_41528502/article/details/110941735