287. 寻找重复数(Find the Duplicate Number)

频次 ★★★★ · 难度 🟡 · 高频:字节/腾讯

题目

n+1 个整数存入长度为 n+1 的数组,数字范围 [1, n],只有一个数字重复(至少出现两次),其余每个出现恰好一次。不修改数组,O(1) 空间,找出重复的那个数。

示例

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

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

思路

Floyd 判圈(快慢指针映射到下标):把数组的下标当成节点,nums[i] 作为 next 指针——从 i 出发下一步到 nums[i]。因为有重复数字,这个「下标→值」映射必然成环,环入口就是重复值。

nums[0] 作为起点,快慢指针同 141/142 的链表判环逻辑:slow 走一步(slow = nums[slow]),fast 走两步(fast = nums[nums[fast]]),相遇后从头出发与 slow 同步前进,相遇点即环入口(重复数)。

代码

public int findDuplicate(int[] nums) {
    int slow = nums[0];
    int fast = nums[0];
    // 第一阶段:找到环内相遇点
    do {
        slow = nums[slow];
        fast = nums[nums[fast]];
    } while (slow != fast);
    // 第二阶段:找环入口(重复数)
    slow = nums[0];
    while (slow != fast) {
        slow = nums[slow];
        fast = nums[fast];
    }
    return slow;
}

复杂度

  • 时间:O(n) —— 每个指针最多走 n 步
  • 空间:O(1)

边界条件

  • n = 1 时,数组 [1,1],slow=nums[0]=1,fast=nums[0]=1,do-while 直接满足条件,第二阶段 slow=nums[0]=1 不动,返回 1 正确
  • 重复数出现超过两次(如 [2,2,2,2]):映射仍会成环,环入口仍是重复值

变式

  • 数组允许修改(排序或下标归位):直接排序找相邻相等,或把每个数放到「值-1」的下标上,第一个冲突的就是重复数。本题限制不修改数组
  • 求重复数具体出现次数:本题只需找出值,不需统计次数。如果需要频次,用哈希表或二分值域(剑指 Offer 的计数法),后者 O(n log n) 但满足 O(1) 空间

易错点

  • 第一阶段用 do-while 而非 while:必须让 slow 和 fast 先走一步再比较,否则初始相等直接退出
  • 把数组当链表时「下标 i 指向 nums[i]」是有向边,节点数是数组长度 n+1,每个节点值域 [1,n] 保证不会越界
  • 本题禁止修改数组(否则可以用原地哈希),也不能用额外哈希集(O(n) 空间不合格)

面试追问

  • 为什么一定有环? 鸽巢原理:n+1 个位置,值域只有 n 个不同值([1,n]),最少有一个值出现两次,映射到下标就形成”两个不同的下标指向同一个值”,构成入环点
  • 能不能用二分法(值域二分)? 可以,统计 ≤mid 的元素个数,如果 count > mid 则重复数在 [1,mid] 范围内,否则在 (mid,n]。时间 O(n log n),空间 O(1),面试时作为另一种 O(1) 空间解法提到即可
  • 如果允许修改数组,最好的解法是什么? 原地归位:遍历时把 nums[i] 放到 nums[nums[i]-1] 位置,如果发现目标位置已经有相同的值,即找到重复数。时间 O(n),空间 O(1)

关联题