【LeetCode Hot100 刷题日记 (100/100)】287. 寻找重复数——数组、双指针、二分查找、位运算、Floyd 判圈🔍

8 阅读5分钟

📌 题目链接:287. 寻找重复数 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:数组、双指针、二分查找、位运算、Floyd 判圈

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(1)


🧠 题目分析

给定一个长度为 n + 1 的整数数组 nums,其中每个元素的取值范围是 [1, n]。题目保证只有一个数字重复出现(可能多次),其余数字均只出现一次。

要求:

  • 不能修改原数组
  • 只能使用 O(1) 的额外空间
  • 理想情况下实现 O(n) 时间复杂度

这道题看似简单,但限制条件非常严格。常见的哈希表(空间 O(n))、排序(修改数组或需额外空间)等方法都被排除。因此,我们需要借助更巧妙的数学或图论思想来解决。

💡 关键洞察:由于 nums[i] ∈ [1, n] 且数组长度为 n+1,我们可以将数组视为一个函数映射
f(i) = nums[i],即从下标 i 跳转到 nums[i]
这样就构成了一个链表结构,而重复的数字会导致多个节点指向同一个位置,从而形成


🔁 核心算法及代码讲解:快慢指针(Floyd 判圈算法)

🎯 算法背景

Floyd 判圈算法(又称“龟兔赛跑算法”)最初用于判断链表是否存在环,并能在线性时间内找到环的入口点。该算法仅使用两个指针,空间复杂度为 O(1),完美契合本题约束。

经典应用:LeetCode 141(判断环)、142(找环入口)

🧩 为什么本题可以建模为“带环链表”?

我们将数组下标 0 ~ n 视为节点,i → nums[i] 作为有向边:

  • 因为 nums[i] ∈ [1, n],所以所有跳转都落在 [1, n] 范围内。
  • 下标 0 是起点(因为 nums[0] 是有效值,且不会被其他位置指向?不一定,但作为起始点可行)。
  • 由于存在重复数字 target,至少有两个不同的 i, j 满足 nums[i] = nums[j] = target,即有两个指针指向 target形成入环点
  • 一旦进入 target,后续路径由 nums[target] 决定,最终会循环(因为有限状态)。

因此,整个结构是一个带环的链表,而重复的数字就是环的入口

🔄 Floyd 算法两阶段

阶段一:检测环(快慢指针相遇)

  • slow 每次走 1 步:slow = nums[slow]
  • fast 每次走 2 步:fast = nums[nums[fast]]
  • 若存在环,二者必在环内某点相遇。

阶段二:找环入口

  • slow 重置为起点 0
  • slowfast 同时每次走 1 步
  • 再次相遇点即为环入口 → 重复数字

📐 数学证明简述(面试高频!): 设起点到环入口距离为 a,环入口到相遇点距离为 b,环剩余部分为 c(即环长 L = b + c)。

  • slow 走了 a + b
  • fast 走了 a + b + kL(k 圈)
  • 又因 fast 速度是 slow 两倍:2(a + b) = a + b + kL ⇒ a = kL - b = (k-1)L + c
  • 所以从起点走 a 步 = 从相遇点走 c 步(再绕 k-1 圈)→ 都到达环入口!

💻 C++ 核心代码(带详细行注释)

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        // 初始化快慢指针,从下标 0 出发
        int slow = 0, 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;                    // 相遇点即为重复数字
    }
};

注意:初始 slow = fast = 0 是合法的,因为 nums[0] 是有效值(∈ [1, n]),且题目未禁止从 0 开始。


🧭 解题思路(分步骤)

  1. 理解问题限制:不能改数组、O(1) 空间 → 排除哈希、排序。
  2. 发现隐藏结构:将数组视为函数 f(i) = nums[i],构建链表。
  3. 识别环的存在:重复数字导致多个前驱 → 必有环。
  4. 应用 Floyd 算法
    • 用快慢指针检测环并找到相遇点。
    • 利用数学性质,从起点和相遇点同步走,找到环入口。
  5. 返回环入口值:即为重复数字。

📊 算法分析

方法时间复杂度空间复杂度是否满足题意
哈希表O(n)O(n)❌ 空间超限
排序O(n log n)O(1)(若允许修改)❌ 不可修改数组
二分查找O(n log n)O(1)✅ 但非最优时间
位运算O(n log n)O(1)✅ 但较难理解
快慢指针(Floyd)O(n)O(1)✅✅✅ 最优解

🎯 面试重点

  • 能否将数组转化为链表模型?
  • 能否解释 Floyd 算法的两阶段原理?
  • 能否手写无 bug 的快慢指针代码?
  • 能否对比其他方法的优劣?

💻 完整代码

C++ 实现

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int slow = 0, 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;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    vector<int> nums1 = {1,3,4,2,2};
    cout << sol.findDuplicate(nums1) << "\n"; // 输出: 2

    vector<int> nums2 = {3,1,3,4,2};
    cout << sol.findDuplicate(nums2) << "\n"; // 输出: 3

    vector<int> nums3 = {3,3,3,3,3};
    cout << sol.findDuplicate(nums3) << "\n"; // 输出: 3

    return 0;
}

JavaScript 实现

/**
 * @param {number[]} nums
 * @return {number}
 */
var findDuplicate = function(nums) {
    let slow = 0, 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;
};

// 测试
console.log(findDuplicate([1,3,4,2,2])); // 2
console.log(findDuplicate([3,1,3,4,2])); // 3
console.log(findDuplicate([3,3,3,3,3])); // 3

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样