287. 寻找重复数

题目

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,找出 这个重复的数 。

示例 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

提示:

2 <= n <= 3 * 104
nums.length == n + 1
1 <= nums[i] <= n
nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

进阶:

如何证明 nums 中至少存在一个重复的数字?
你可以在不修改数组 nums 的情况下解决这个问题吗?
你可以只用常量级 O(1) 的额外空间解决这个问题吗?
你可以设计一个时间复杂度小于 O(n2) 的解决方案吗?

方法一:位运算

   public int findDuplicate(int[] nums) {
        //数组元素值在1-n-1之间
        int res=0,n=nums.length,digit,cnt1,cnt2;
        for(int i=0;i<32;++i){
            digit=1<<i,cnt1=0,cnt2=0;
            for(int k=0;k<n;++k){
                //1-n-1所有数和digit进行与操作
                if((k&digit)>0) cnt1++;
                //nums数组所有元素和digit进行与操作
                if((nums[k]&digit)>0) cnt2++;
            }
            //只用考虑重复的数的二进制形式哪一位是1
            if(cnt2>cnt1) res+=digit;
        }
        return res;
    }

方法二:二分法

    public int findDuplicate(int[] nums) {
        int n=nums.length-1,left=1,right=n,mid,cnt;
        while(left<right){
            mid=(left+right)/2;
            cnt=0;
            //统计数组中小于等于mid的元素个数
            for(int i=0;i<n+1;++i){
                if(nums[i]<=mid) cnt++;
            }
            //如果重复数在[1,mid]范围内,则cnt必定大于mid
            //因为此时[mid+1,n]范围内不存在重复数,最多只能有n-mid个数
            if(cnt<=mid) left=mid+1;
            else right=mid;
        }
        return left;
    }

方法三:快慢指针

如何将数组看成链表

如果数组中没有重复的数,以数组 [1,3,4,2]为例,我们将数组下标 n 和数 nums[n] 建立一个映射关系 f(n)f(n),
其映射关系 n->f(n)为:
0->1
1->3
2->4
3->2
我们从下标为 0 出发,根据 f(n)f(n) 计算出一个值,以这个值为新的下标,再用这个函数计算,以此类推,直到下标超界。这样可以产生一个类似链表一样的序列。
0->1->3->2->4->null

如果数组中有重复的数,以数组 [1,3,4,2,2] 为例,我们将数组下标 n 和数 nums[n] 建立一个映射关系 f(n)f(n),
其映射关系 n->f(n) 为:
0->1
1->3
2->4
3->2
4->2
同样的,我们从下标为 0 出发,根据 f(n)f(n) 计算出一个值,以这个值为新的下标,再用这个函数计算,以此类推产生一个类似链表一样的序列。
0->1->3->2->4->2->4->2->……
这里 2->4 是一个循环,那么这个链表可以抽象为下图:

从理论上讲,数组中如果有重复的数,那么就会产生多对一的映射,这样,形成的链表就一定会有环路了,

综上
1.数组中有一个重复的整数 <==> 链表中存在环
2.找到数组中的重复整数 <==> 找到链表的环入口

如果i=nums[i]不就自己成环了吗

以{2,1,3,4}为例,有0->2, 1->1, 2->3, 3->4,得到链表0->2->3->4,可以看出:当i=nums[i]时,链表并不会包含i这个数,因为链表延伸需要下标和数组元素交替连接。而数组中又不包含0,所以从0出发可以得到最终的链表。

链表中的环

image.png

如上图,slow和fast会在环中相遇,先假设一些量:起点到环的入口长度为m,环的周长为c,在fast和slow相遇时slow走了n步。则fast走了2n步,fast比slow多走了n步,而这n步全用在了在环里循环(n%c==0)。
当fast和last相遇之后,我们设置第三个指针t,它从起点开始和slow(在fast和slow相遇处)同步前进,当t和slow相遇时,就是在环的入口处相遇,也就是重复的那个数字相遇。

为什么 t 和 slow 相遇在入口

fast 和 slow 相遇时,slow 在环中行进的距离是n-m,其中 n%c==0。这时我们再让 slow 前进 m 步——也就是在环中走了 n 步了。而 n%c==0 即 slow 在环里面走的距离是环的周长的整数倍,就回到了环的入口了,而入口就是重复的数字。

   public int findDuplicate(int[] nums) {
        int slow=0,fast=0,t=0;
        //当slow和fast在环中相遇时结束循环
        while(true){
            slow=nums[slow];
            fast=nums[nums[fast]];
            if(slow==fast) break;
        }
        //找到环入口时结束循环
        while(true){
            slow=nums[slow];
            t=nums[t];
            if(slow==t) break;
        }
        return slow;
    }

这种方法介绍了一种找到环入口的方法。之前我们做过检测链表中是否有环的问题,那么对于进阶问题————找到链表中环的入口,现在就有了一种解决思路。

参考

[1]:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/287xun-zhao-zhong-fu-shu-by-kirsche/
[2]:https://leetcode-cn.com/problems/find-the-duplicate-number/solution/kuai-man-zhi-zhen-de-jie-shi-cong-damien_undoxie-d/


来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-the-duplicate-number
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

原文地址:https://www.cnblogs.com/Frank-Hong/p/14783220.html