287. Find the Duplicate Number(链表判环)

本文介绍了一种在不修改原始数组的情况下,使用常数级额外空间找到重复数字的方法。主要讨论了四种解决方案:哈希表、暴力法、排序法及二分法,并详细解析了二分法和映射找环法的实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.

Note:

  1. You must not modify the array (assume the array is read only).
  2. You must use only constant, O(1) extra space.
  3. Your runtime complexity should be less than O(n2).
  4. There is only one duplicate number in the array, but it could be repeated more than once.

首先讲一下我的思路:

1.哈希表

对于这种数组中找重复元素的问题,我当然第一时间想到哈希表,但是哈希表空间复杂度是O(n),所以不能用。

2.暴力法

两个循环就可以暴力查找出重复的元素,时间复杂度是O(n^2),题目要求less than O(n^2),所以排除。

4.排序

时间是可以,但会改变数组,所以不可取。

5.二分法

我想到了二分法,但是刚开始我思路走入了误区。一共n+1个元素,每个元素都是在[1..n]范围之内,那么除过一个重复的,剩下的肯定就是无重复的且范围在[1..n]之内的元素了。

况且,这道题二分法和一般的不一样,这道题的思想是这样的。既然有元素值的范围,我们对元素值的范围进行二分,而不是对数组二分,然后通过统计数组中元素个数来加上限制条件,缩小元素值范围继续二分下去。

比如:对于元素集合[1..10],现在有n+1个元素,即10+1=11个,那么必定有一个重复元素。

我们首先将元素集合分为[1..5] 和 [6..10],此刻mid=5。通常来说,如果遍历数组得到<=5的元素数目和mid相等,则说明重复元素在右半区间中。否则,在左半区间中。然后继续二分下去,直到最终只有一个元素。例如:元素4重复了,那么数组中会有1,2,3,4,4,5,一共6个元素,这不正说明重复元素在mid即5的左半区间吗?

由于一直二分下去,最终剩下两个元素的时候,high会向low靠拢,所以我们返回low即可。

代码如下:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        size_t size = nums.size();
        if(size ==0 || size == 1)
            return -1; 
    
        return binsearch_like(nums, size, 1, size-1);
    }   
private:
    int binsearch_like(vector<int>& nums, int size, int low, int high){
        while(low < high){
            int mid = low + (int)((high-low)>>1);
            int count = 0;
            for(int i=0; i<size; ++i){
                if(nums[i] <= mid)
                    ++count;
            }   
            if(count > mid)
                high = mid;
            else
                low = mid+1;
        }   
        return low;
    }
};


4.映射找环法

对于数组a[3] = {1, 2, 3},我们把它看作下标和元素值的映射。0->1, 1->2, 2->3, 连起来就是 0->1->2->3。

既然存在重复元素,例如数组a[5] = {1, 2, 3, 3, 4}, 连起来就是 0->1->2->3->3->3...无限环转圈

或者数组a[6] = {1, 2, 4, 5, 3, 3},连起来就是0->1->2->4->3->5->3->5->3->5->3....同样无线循环转圈

这道题这就转换成了链表求环的问题。

这种题的通常解法就是一个快指针,一个慢指针。快指针步长为满指针的2倍。就好像在操场跑步,一个人跑的快,一个人跑的慢,只要操场是个环,那么跑的快的总会追上跑得慢的。追上的点就是有环点。

由于步长的

然后再定义一个指针从0开始,之前的指针从有环点开始,两个相遇点即为重复元素所在。

代码如下:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        size_t size = nums.size();
        if(size ==0 || size == 1)
            return -1; 
    
        int slow = 0;
        int fast = 0;
        do{
            slow = nums[slow];
            fast = nums[nums[fast]];
        }while(slow != fast);
        
        int find = 0;
        while(find != slow){
            find = nums[find];
            slow = nums[slow];
        }
        return slow;
    }   
};

证明如下:

如果fast和slow相遇,那么在相遇时,slow肯定没有遍历完链表,而fast已经在环内循环了n圈(n>=1)(见下面的证明2)。假设 slow走了s步,则fast走了2s步(fast的步数还等于s加上在环上多转的n圈),设环长为r,则: 
2s=s+nr 
s=nr 
设整个链表长L,入口环与相遇点距离为x,起点到环入口点的距离为a,则 
a+x=s=nr 
a+x=(n-1)r+r=(n-1)r+L-a 
a=(n-1)r+(L-a-x) 
(L-a-x)为相遇点到环入口点的距离。由此可知,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点。于是可以从链表头和相遇点分别设一个 指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值