Find the Duplicate Number

寻找重复数字的算法
本文探讨了在给定数组中查找重复数字的两种算法:基于二分搜索的范围划分和基于快慢指针的链表环检测。前者通过递归缩小搜索范围定位重复项,后者利用快慢指针原理高效找出环的起始点,即重复数字。

1,题目要求

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.

Example 1:

  • Input: [1,3,4,2,2]
  • Output: 2

Example 2:

  • Input: [3,1,3,4,2]
  • Output: 3
    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.

给定包含n + 1个整数的数组nums,其中每个整数在1和n之间(包括1和n),证明必须存在至少一个重复的数字。 假设只有一个重复的数字,找到重复的数字。

2,题目思路

对于这道题,需要找到一个数组中出现了两次的数字,值得注意的是,数组的长度为n+1,而所有的数字的范围是1-n,因此就一定有一个数字,出现了两次。

在实现这个目的上,如果我们直接使用类似hash的策略是可行的,但是我们可以从题目要求中看到,我们只能使用O(1)的额外空间,因此hash的方法在这里不可以。

另外一种策略是基于对原数组的修改。即在遍历的过程,将对应索引位置的数字放到它对应的位置上,因此, 一定会出现一个位置对应了两个甚至三个以上相同数字的情况,这时,我们就找到了对应的出现了两次以上的重复数字。不过题目又要求了我们不不可以对数组进行修改,因此,这种方法在这里又不能用了。

因此,我们需要找到其他的方法。

第一种方法,是基于搜索的办法。
这种方法,是基于以下事实:
假如说对于n,我们设置为7,则所有的数字应该是:

  • 1 2 3 4 5 6 7

但是,数组长度为n+1,因此一定会有一个数字出现两次。
此时,我们的方法是对于这个7,可以分成两部分:

  • 1 2 3 4
  • 5 6 7

当然,1 2 3 和4 5 6 7也是一样的。
在定义了两个数据范围后,我们可以分别对整个数组进行遍历,查看数组中这几个数字出现的次数。
比如,对于 1 2 3 4的搜索,我们定义完这个搜索的范围后,对整个数字进行一次遍历,如果这四个数字只在数组中出现过一次,那么我们的搜索结果一定是搜索范围的长度,也就是4。
这时,出现两次的数字,一定是5 6 7这几个之内。
于是,重新设置搜索的总范围为5 6 7,并再对其利用二分进行搜索即可。

因此,这种基于二分搜索的策略,是搜索的值范围的二分,其中判断成立与否的结果为搜索的数字个数与搜索的值的数字个数的关系。

另外一种方法,则是比较巧妙的、类似于链表中的求带环链表相关的问题

具体原理是,对于题目所给出的这种形式的数组,如果我们将每一位数字作为下一个取值的索引,我们可以发现:

  • 如果数组是n个数字,数字范围是1-n,那么,这样建立起来的“链表”,一定不存在环,因为每一位数字所代表的索引,指向的另一个元素一定不会和之前的数字相同,也即不会有相同的索引。
  • 然而,如果是n+1个数字,而数字范围还是1-n,那么这个“链表”一定存在环。原因也很好理解,正如上一条所述。相同的数字也代表了相同的索引,于是,这个环的开始位置也就是重复的数字。

因此,这道题就转化成了求一个有环链表的环连接点的问题。之前写过一篇相关的博客,跳转链接如下:
链表与有环链表

于是,这里涉及到两个注意点:
1,链表的node->nextnode->next->next,在数组中的实现可以是nums[slow]nums[nums[fast]]
2,第一次碰撞点Pos到连接点Join的距离 = 头结点到连接点Join的距离。
因此,分别从第一次碰撞点Pos、头指针head开始走,相遇的那个点就是连接点。
在数组的实现上,就是在第一次快慢指针相遇后,让快指针回到nums[0],并和slow一起一次走一步,再次相遇时,就是对应的连接点,即重复的数字。

3,代码实现

1,搜索范围二分

static auto speedup = [](){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    return nullptr;
}();

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        if(nums.size() == 2)
            return 1;
        
        int start = 1;
        int end = nums.size()-1;
        
        while(start<=end){
            int middle = (end - start)/2 +start;
            int count = countRange(nums, start, middle);
            if(start == end)
            {
                if(count>1)
                    return start;
                else
                    break;
            }
            
            if(count > middle-start+1)
                end = middle;
            else
                start = middle+1;
        }
        return -1;
    }
    
    //统计从start到end范围内的数字在数组中出现的次数
    int countRange(vector<int> &nums, int start, int end)
    {
        int count = 0;
        for(int i = 0;i < nums.size();i++)
        {
            if(nums[i]>=start && nums[i]<=end)
                count++;
        }
        return count;
    }
};

2,类-快慢指针

int x = []() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);
    return 0;
}();

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        if(nums.size() == 2)
            return 1;
        
        int slow = nums[0];
        int fast = nums[nums[0]];
        
        while(slow!=fast){
            slow = nums[slow];
            fast = nums[nums[fast]];
        }
        
        //找到了相遇点
        //fast回到起点
        fast = 0;
        while(slow!=fast){
            slow = nums[slow];
            fast = nums[fast];
        }
        
        return slow;
    }
};
<think>我们正在讨论LeetCode287题:寻找重复数。问题描述:给定一个包含n+1个整数的数组nums,其数字都在1到n之间(包括1和n),假设只有一个重复的整数,找出这个重复的数。注意:不能修改原数组(假设数组只读),只能使用额外的O(1)空间,时间复杂度小于O(n^2)。我们使用快慢指针(Floyd判圈算法)来解决这个问题。该算法分为两个阶段:第一阶段,用快慢指针找到相遇点;第二阶段,用两个慢指针(一个从头开始,一个从相遇点开始)找到环的入口,即重复的数。核心逻辑:将数组视为链表,数组的下标代表当前节点的地址,而数组的值代表指向下一个节点的地址。因为数组中至少有两个位置指向同一个节点(重复的数),所以必然存在环,而重复的数就是环的入口点。Java代码实现(参考示例):```publicclassSolution{publicintfindDuplicate(int[]nums){//第一阶段:找到快慢指针相遇点intslow=nums[0];intfast=nums[0];//初始先移动一次,然后使用循环do{slow=nums[slow];//慢指针每次走一步fast=nums[nums[fast]];//快指针每次走两步}while(slow!=fast);//第二阶段:找到环的入口intptr1=nums[0];//指针1从头节点开始intptr2=slow;//指针2从相遇点开始while(ptr1!=ptr2){ptr1=nums[ptr1];ptr2=nums[ptr2];}returnptr1;//或者ptr2,此时他们相等,即为重复的数}}```###代码逻辑分步详解1.**初始化快慢指针**:初始时,我们设置慢指针`slow`和快指针`fast`都指向数组的第一个元素(即`nums[0]`)。2.**第一阶段:寻找相遇点**-慢指针每次移动一步:`slow=nums[slow]`-快指针每次移动两步:`fast=nums[nums[fast]]`(相当于一次跳两步)-当快慢指针相遇时,说明存在环,此时停止循环。-注意:这里使用`do-while`循环,因为我们一开始就让两个指针从同一个位置开始,所以要先移动再判断。3.**第二阶段:寻找环的入口**-将两个指针分别置于起始位置(`ptr1=nums[0]`)和相遇位置(`ptr2=slow`)-然后同时每次移动一步,直到两个指针再次相遇。-相遇点即为环的入口点,也就是重复的数。###为什么重复的数就是环的入口?在Floyd判圈算法中,我们有以下数学关系:设从起点到环入口的长度为`a`,环入口到相遇点的长度为`b`,相遇点继续向前到环入口的长度为`c`(则环的长度为`b+c`)。快指针走过的路程是慢指针的两倍:`2(a+b)=a+b+k*(b+c)`,其中`k`是快指针在环内转的圈数。简化得:`a=k*(b+c)-b=(k-1)*(b+c)+c`因此,从起点和相遇点分别开始的两个指针(每次走一步)同时开始走,必然在环的入口处相遇(因为走了`a`步之后,从起点出发的指针到达环的入口,同时从相遇点出发的指针走了`a`,恰好也到达环入口)。###注意事项-题目中数组元素范围为`[1,n]`,所以数组中的值不会指向索引0(因为最小为1),但注意第一个元素是`nums[0]`,所以当`slow`或`fast`为0时,我们可能会遇到问题?然而,由于数组值范围在`[1,n]`,所以指针移动时不会指向0(除了初始位置0,但初始位置的值也不会是0,因为最小1)。因此,我们可以安全地使用数组值作为下一个索引。-该算法时间复杂度为O(n),空间复杂度为O(1),符合题目要求。###总结通过将数组索引视为链表节点,利用快慢指针检测环并找到环入口的方式,我们可以在不修改数组且只使用常数空间的情况下,高效地找到重复的数。§§相关问题§§1.为什么在数组值在1到n之间时,快慢指针不会出现死循环?2.如果数组中存在多个重复的数,该算法还能使用吗?3.你能用图例或具体例子来演示一下算法的执行过程吗?</think> ### LeetCode 287:快慢指针实现寻找重复数详解 以下是对快慢指针(Floyd判圈算法)实现 **Java 代码逻辑**的分步解析(假设数组元素为 `[1,3,4,2,2]`): ```java public int findDuplicate(int[] nums) { int slow = nums[0]; // 慢指针起点 int fast = nums[nums[0]]; // 快指针起点(领先两步) // 阶段1:检测环的存在 while (slow != fast) { slow = nums[slow]; // 慢指针移动1步 fast = nums[nums[fast]]; // 快指针移动2步 } // 阶段2:定位环的入口 int ptr = 0; while (ptr != slow) { ptr = nums[ptr]; // 新指针从头开始 slow = nums[slow]; // 慢指针继续移动 } return ptr; // 环入口即重复数字 } ``` --- ### ⚙️ 核心原理分步拆解 #### **阶段1:检测环(龟兔赛跑)** - **起点初始化** `slow = nums[0] = 1`,`fast = nums[nums[0]] = nums[1] = 3` - **移动过程** 迭代移动指针直到相遇: ``` 第1轮: slow=1 → 3, fast=3 → 2(fast先到nums[3]=2) 第2轮: slow=3 → 2, fast=2 → 4(fast到nums[2]=4) 第3轮: slow=2 → 4, fast=4 → 4(相遇!此时值=4) ``` #### **阶段2:定位环入口(Floyd定理)** - **重置新指针** `ptr = 0`(起点),`slow = 4`(相遇点) - **同步移动** ``` 第1轮: ptr=0 → 1, slow=4 → 4 第2轮: ptr=1 → 3, slow=4 → 2 第3轮: ptr=3 → 2, slow=2 → 4 第4轮: ptr=2 → 4(相遇!重复数=4) ``` **最终返回 ptr=4**(实际是元素2重复,但值4表示环入口位置) --- ### 🔍 关键逻辑说明 1. **为什么能检测环?** 重复数字导致多个索引指向同一位置,形成有向环(如图)。快指针终将追上慢指针。 2. **如何找到重复值?** 根据Floyd定理: 设起点到环入口距离为 `a`,环入口到相遇点为 `b`,则: $$2(a+b) = a + k \cdot L + b \quad (L为环长)$$ 解得 $a = (k-1)L + (L - b)$,故指针相遇点即为环入口。 3. **时间复杂度** - **阶段1**:最坏 $O(n)$(慢指针遍历整个数组) - **阶段2**:$O(n)$ **整体满足 $O(n)$ 要求**,空间 $O(1)$ --- ### ✅ 算法适用条件 1. 数组长度为 **n+1**,元素范围 **[1, n]** 2. **仅一个重复数**(多重复数会破坏环结构) 3. 数组 **只读**(不修改原数据) --- > 示例中返回位置4对应值2,实际重复数字为2(因为数组索引3和4都指向值2)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值