LeetCode - Find the Duplicate Number

本文介绍了一种使用O(1)空间复杂度及低于O(n²)时间复杂度寻找数组中重复数字的方法。通过将数组视作链表并利用快慢指针技术找到环的入口,从而确定重复的数字。

摘要生成于 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:

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

分析

这个题是两部分问题,第一部分的证明看起来比较直接,在数学上可以通过反证法证明。

第二部分是说现在有 n+1 个位置,放进去的数字都是从 1 到 n 的,但是存在一个重复的数字,其他的都不重复,让用 O(1) 的空间复杂度和少于O(n2)的时间复杂度找出这个重复的数字。示例输入输出可能是

[2,3,1,2] -> 2
[2,2,1,2,2] -> 2

一个比较详细的解答可以参考
https://segmentfault.com/a/1190000003817671

这里我想解释一下链接中的最后一个方法:映射找环法。代码可以参考上述链接,这里只是将其原理进行一个详细的论述

可以先看一下上述链接中的解释,这个算法的思路如下:

将数组中每个元素都看成链表中的一个节点,链表节点有两个基本要素就是节点的值(val)和指向下一个节点的指针(next),我们假设这个数组表示成 array[], 某个节点的索引是 index,在这个算法所想象的链表中:

  • 节点的值 val 就是数组在这个位置的值 array[index]
  • 节点的指向想一个节点的指针 next 也是数组在这个位置的值 array[index]

举个例子,数组 [2,3,1,2] 构成的链表如下图所示

这里写图片描述

我们发现这个图中出现了一个环,下面我们说明在给定的条件下,从数组的第0个元素出发,一定会进入一个环中,且这个环包含了重复的那个数字:

  1. 假设从第0个元素出发,最后没进入环中(反证),这就说明由0出发的链表的尾节点的next是null
  2. 然而给定条件下,数组中每个元素的值都是1 - n之间的数字(同时根据之前的构造方法数组的值也就是其next指针),说明这个链表中不会出现指向null的节点

上面的1和2相互矛盾,说明从数组的第0个元素出发,一定会进入一个环中

我们知道,指向环的入口元素(上例中的array[2]=1)的指针值(next)相同,而根据构next又等于节点的val,说明换入口元素的上一个节点就是重复的那个值。

事实上构造出来的图中可能存在多个环,但由于题的条件限定了只可能有一个重复元素,所以其中只有一个环会带着一个或多个“尾巴”,这个“尾巴”是进入环之前的部分。这个题的巧妙之处就在于数组中元素都在1-n之间,说明不会有节点的next值等于0,即array[0]一定是这样的一个“尾巴”的头部,而不会出现在环的内部。

这样原问题就转化为了在单链表中找环入口的问题(重复的数字是环入口前一个节点),利用快慢指针的办法可以很容易的写出代码(网上有很多资料,也可以看上文链接中的代码)

总结一下上面的几个关键点

  • 将数组看成一个链表,链表的 val 和 next 都是数组元素值
  • 从第0个元素出发,必然会形成环
  • 重复的数字节点在环的入口的前一个节点处

最后我想说的是,用数组表示链表或树状也是一种常见的办法,效率很高,以前学数据结构的时候学到过但后来一直没用过,可以看看 java 的 TImer 类的源代码中最小堆是如何实现的。

<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、付费专栏及课程。

余额充值