题目描述

✅ 287. 寻找重复数

image-20260928200241681

image-20260928200241682

题意分析

数组长度为 n + 1,每个值都在 1 到 n 之间,其中只有一种数值重复出现,返回这个重复值。它可能出现两次,也可能出现更多次;要找的是数值,不是重复位置。

n + 1 次出现只能放进 n 种取值中,至少一种值必然重复,这就是抽屉原理。要求不能修改输入数组,只使用常数额外空间,进阶还要求线性时间。因此不能通过排序、原地标记或保存全部已见元素来规避这些限制。

解法:快慢指针找环入口

核心思路

[!blue]

把每个下标看作节点,并定义它的下一跳为 next(i) = nums[i]。由于数值都在 1..n,每次读取出的值也是合法下标,可以不断沿这个映射前进。无需创建链表,数组本身已经提供了所有下一跳。

从下标 0 出发,路径会进入有限个节点中的某个环。没有任何数值为零,所以路径不可能返回起点 0,它必定位于环外。进入环的第一个节点,既有来自环外路径的前驱,也有来自环内的前驱;这两个不同下标指向相同值,说明环入口编号就是一个重复数。题目只允许一种重复值,所以它就是答案。

先用 Floyd 快慢指针找环内相遇点。慢指针每次走一步,快指针每次走两步;慢指针进入环后,快指针在环中相对它每轮多走一步,最终一定追上。初始化两者都在零时,要先移动再判断,不能把出发时的相等误当成环内相遇。

第一次相遇不一定在入口,还需要第二阶段。设起点到入口的距离为 a,环长为 c,相遇时慢指针已走 t 步。快指针多走的 t 步是整圈数,因此 t 是 c 的倍数;相遇点相对入口的位置为 (t - a) mod c。让相遇点再走 a 步,就会回到入口。

因此把一个指针重置到 0,另一个留在相遇点,两者都每次走一步。经过 a 步,前者刚到入口,后者也回到入口;此前一个仍在环外、另一个一直在环内,不可能提前相遇。第二次相遇的编号就是重复值,直接返回即可。

解题步骤

  1. 令 slow = fast = 0,用数组值表示下一跳。
  2. 先移动:slow = nums[slow],fast = nums[nums[fast]];相等时结束第一阶段。
  3. 把 slow 重置到 0,保持 fast 在相遇点。
  4. 两者都每次走一步,直到再次相等。
  5. 返回相遇的编号,它就是重复的数值。

代码实现

class Solution {
    public int findDuplicate(int[] nums) {
        int slow = 0;
        int fast = 0;

        // 先移动再判断相遇,零是虚拟起点,不是重复数答案。
        do {
            slow = nums[slow];
            fast = nums[nums[fast]];
        } while (slow != fast);

        // 相遇后重置一个指针,两者同速找到隐式链表的入口。
        slow = 0;

        while (slow != fast) {
            slow = nums[slow];
            fast = nums[fast];
        }

        return slow;
    }
}
func findDuplicate(nums []int) int {
    slow, fast := 0, 0
    // 先移动再判断相遇,零是虚拟起点,不是重复数答案。
    for {
        slow = nums[slow]
        fast = nums[nums[fast]]
        if slow == fast {
            break
        }
    }

    // 相遇后重置一个指针,两者同速找到隐式链表的入口。
    slow = 0
    for slow != fast {
        slow = nums[slow]
        fast = nums[fast]
    }
    return slow
}

复杂度分析

  • 时间复杂度:$O(n)$。第一阶段进入环并相遇的步数为尾长与环长之和的量级,第二阶段只走尾长;可达节点总数不超过 n + 1。
  • 空间复杂度:$O(1)$,只保存两个下标,不创建链表、不修改数组。

关键点总结

[!green]

  • 值域落在合法下标范围内,允许把数组解释为每个节点只有一个后继的图。
  • 从零出发时起点必在环外,环入口有两个不同前驱,因此对应重复值。
  • 第一阶段利用不同速度相遇,第二阶段利用路程关系定位入口,职责不同。

易错点总结

[!yellow]

  • 返回第一阶段相遇点,只能得到环内某个节点,未必是入口。
  • 从相同的零下标初始化后先检查相等,会跳过整个找环过程;必须先移动。
  • 第二阶段应回到本实现的起点 0,并让两个指针都走一步,不能沿用快指针两步的速度。
  • 找到入口后应返回它的编号,不是再返回 nums[入口];入口的下一跳未必仍是重复值。
  • 只凭求和或异或推出一个重复值,隐含了其他取值出现次数的额外限制,本题不保证重复值只出现两次。
  • 排序或原地改符号会修改输入,哈希集合会使用线性空间,都不符合本题限制。

相似题目

题目 难度 关联与区别
142. 环形链表 II 中等 把数组值当作下一下标后形成函数图,重复值对应环入口,可复用快慢指针找入口。
141. 环形链表 简单 用快慢指针确定环或中间位置;本题将数组值视为后继指针寻找重复入口,该题判断链表是否存在环。
202. 快乐数 简单 用快慢指针确定环或中间位置;本题将数组值视为后继指针寻找重复入口,该题将数字变换视为隐式后继关系。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/75515068
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!