【中等】287. 寻找重复数

0 阅读3分钟

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:

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

示例 2:

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

示例 3 :

输入: nums = [3,3,3,3,3]
输出: 3

提示:

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

进阶:

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

1. 核心矛盾:带着镣铐起舞

这道题最狠的地方在于它的限制条件

  • 不能修改数组(不能排序,不能原地打标记)。
  • 只能用 O(1)O(1) 额外空间(不能用哈希表/Set)。
  • 时间复杂度要快(通常指 O(n)O(n))。

2. 生活案例:迷宫里的“死循环”

想象你在玩一个**“盲盒迷宫”**游戏:

  • 迷宫里有很多房间,每个房间都有一个编号
  • 每个房间中心都有一张纸条,上面写着下一个房间的编号
  • 规则是: 按照纸条上的数字走。

为什么会有重复数字?

如果两个不同的房间里,纸条上写了同一个编号(重复数字),这意味着有两条路通向同一个房间。一旦你进入这个结构,你就会陷入一个圆形死循环,永远走不出来。而那个循环的入口,就是我们要找的重复数字。


3. 代码实现与注释(JavaScript)

你可以直接把这段带详尽注释的代码贴进笔记里:

JavaScript

/**
 * @param {number[]} nums
 * @return {number}
 */
var findDuplicate = function(nums) {
    // --- 第一阶段:寻找相遇点(确定迷宫里有环) ---
    // 我们派两名选手:slow(乌龟,每次走一步)和 fast(兔子,每次走两步)
    let slow = nums[0];
    let fast = nums[nums[0]];

    // 只要有重复数字,兔子最终一定会在环里追上乌龟(相遇)
    while (slow !== fast) {
        slow = nums[slow];             // 乌龟走一步
        fast = nums[nums[fast]];       // 兔子跑两步
    }

    // --- 第二阶段:寻找环入口(抓到那个重复的数) ---
    // 此时两人相遇了。根据数学推导:
    // 从“迷宫起点”到“环入口”的距离,正好等于从“当前相遇点”绕环回到“环入口”的距离。
    
    // 我们让🐢回到起点(坐标 0),并🐰降速变得和乌龟一样慢(每次走一步)
    slow = 0; 
    while (slow !== fast) {
        // 两人以同样的速度前进
        slow = nums[slow];
        fast = nums[fast];
    }

    // 当他们再次相遇时,所在的房间编号就是环的入口,也就是那个重复的数字!
    return slow;
};

4. 深度解析:为什么这样能成功?

在掘金分享时,建议加上这几点深度思考,会显得内容很有厚度:

  • 抽象转化: 题目把数字范围限制在 [1,n][1, n],而数组长度是 n+1n+1。这意味着我们可以把 nums[i] 看作是一个指向下标 i 的指针。

  • 数学逻辑: 假设起点到环入口距离为 aa,入口到相遇点距离为 bb,环长度为 LL

    • 2×乌龟路程=兔子路程2 \times \text{乌龟路程} = \text{兔子路程}
    • 2(a+b)=a+b+kL2(a + b) = a + b + kL
    • 推导出 a=kLba = kL - b,这意味着从起点出发走 aa 步,和从相遇点出发走 aa 步,最终都会停在环入口。
  • 复杂度优势: 时间复杂度 O(n)O(n),空间复杂度 O(1)O(1),完美契合题目严苛的要求。