📌 题目链接: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 slow和fast同时每次走 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 开始。
🧭 解题思路(分步骤)
- 理解问题限制:不能改数组、O(1) 空间 → 排除哈希、排序。
- 发现隐藏结构:将数组视为函数
f(i) = nums[i],构建链表。 - 识别环的存在:重复数字导致多个前驱 → 必有环。
- 应用 Floyd 算法:
- 用快慢指针检测环并找到相遇点。
- 利用数学性质,从起点和相遇点同步走,找到环入口。
- 返回环入口值:即为重复数字。
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 是否满足题意 |
|---|---|---|---|
| 哈希表 | 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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样