LeetCode 287. 寻找重复数:从直觉到 Floyd 判圈的完整推导

一、题目描述

给定一个长度为 n + 1的数组 nums,其中:

  • 所有数字都在 [1, n]范围内

  • 只有一个重复的数字(但可能重复多次)

请找出这个重复的数字。

⚠️ 限制条件(重点)

  • ❌ 不能修改原数组

  • ✅ 只使用 **O(1)**​ 额外空间

示例:

复制代码
输入:nums = [1,3,4,2,2]
输出:2

输入:nums = [3,1,3,4,2]
输出:3

二、为什么这题"看起来简单但很难"

如果你允许修改数组,这题很简单:

方法 思路 问题
排序 相邻比较 修改数组 ❌
HashSet 记录出现次数 额外空间 ❌

但题目明确要求:

不能改数组 + O(1) 空间

👉 于是我们必须把 "数组本身"当成某种结构来推理


三、核心洞察:把数组看成「链表」

1️⃣ 关键抽象

数组下标:0 ~ n

数组取值:1 ~ n

我们可以把数组理解成一个 映射函数

复制代码
f(i) = nums[i]

从任意位置 i出发,不断执行:

复制代码
i → nums[i] → nums[nums[i]] → ...

👉 这本质上是一条 链表路径


2️⃣ 为什么一定有环?

  • 节点数:n + 1

  • 值域:1 ~ n(没有 0)

➡️ 必然存在某个节点被指向多次

➡️ 必然形成环

而:

环的入口,就是那个重复的数字


四、Floyd 判圈算法(快慢指针)

这正是 Linked List Cycle II​ 的翻版。

Step 1:找相遇点(快慢指针)

复制代码
slow = nums[slow]
fast = nums[nums[fast]]

最终它们一定在 环内某点相遇


Step 2:找环的入口(重复数)

将其中一个指针重置到起点:

复制代码
slow = 0

然后同步前进:

复制代码
slow = nums[slow]
fast = nums[fast]

再次相遇的位置,就是 重复的数字


五、结合例子理解(非常关键)

nums = [1,3,4,2,2]为例:

索引路径:

复制代码
0 → 1 → 3 → 2 → 4 → 2 → 4 → ...

结构如下:

复制代码
0 → 1 → 3 → 2
            ↓
            4
  • 环:2 → 4 → 2

  • 环的入口:2

  • ✅ 答案就是 2


六、为什么这个方法一定正确?

阶段 目的
快慢指针相遇 证明存在环
重置一个指针 对齐起点与环入口距离
同步移动 数学上必然在入口相遇

这是一个 严格可证明​ 的算法,不是"玄学"。


七、Java 实现(面试标准版)

复制代码
class Solution {
    public int findDuplicate(int[] nums) {
        int slow = 0, fast = 0;

        // Step 1: 找到相遇点
        do {
            slow = nums[slow];
            fast = nums[nums[fast]];
        } while (slow != fast);

        // Step 2: 找到环的入口(重复数)
        slow = 0;
        while (slow != fast) {
            slow = nums[slow];
            fast = nums[fast];
        }

        return slow;
    }
}

八、复杂度分析

指标 数值
时间复杂度 O(n)
空间复杂度 O(1) ✅
是否修改数组

完全满足题目所有限制。


九、易错点总结(面试高频)

不要尝试排序 / HashMap / 计数数组

错误思路 原因
排序 修改数组
HashSet 额外空间
二分 可行但不是最优直观解

面试加分点

"这题本质是一个隐式链表 + Floyd 判圈问题。"


十、总结一句话

解法 特点
暴力 / 排序 ❌ 不满足限制
哈希表 ❌ 空间超限
Floyd 判圈 ✅ 最优解

📌 记忆口诀

数组当链表,快慢找相遇,重置再同步,入口即答案