1. 题目解析与核心思路
这道LeetCode中等难度题目(编号287)要求在一个包含n+1个整数的数组中找到重复的数字,其中所有整数都在1到n之间(包括1和n)。题目保证至少存在一个重复数字,但可能有多个重复项。
1.1 问题重述
给定一个整数数组nums,其中:
- 数组长度为n+1
- 所有元素取值范围为[1, n]
- 至少存在一个重复数字
要求:
- 找出任意一个重复数字
- 不能修改原数组(假设数组是只读的)
- 只能使用O(1)的额外空间
- 时间复杂度应低于O(n²)
1.2 关键约束分析
这道题的特殊之处在于它的限制条件:
- 不能修改原数组:排除了排序后遍历的解法
- O(1)空间复杂度:排除了使用哈希表的解法
- 时间复杂度限制:排除了暴力双重循环
这些限制条件实际上是在引导我们使用更巧妙的算法来解决这个问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 常见解法对比
2.1 暴力解法(不符合要求)
最直观的解法是双重循环:
python复制for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] == nums[j]:
return nums[i]
时间复杂度O(n²),空间复杂度O(1),但不符合题目对时间复杂度的要求。
2.2 哈希表解法(不符合要求)
使用集合存储已出现的数字:
python复制seen = set()
for num in nums:
if num in seen:
return num
seen.add(num)
时间复杂度O(n),但空间复杂度O(n),不符合空间要求。
2.3 排序解法(不符合要求)
先排序再查找相邻重复元素:
python复制nums.sort()
for i in range(1, len(nums)):
if nums[i] == nums[i-1]:
return nums[i]
时间复杂度O(nlogn),空间复杂度O(1)(如果允许修改原数组),但题目明确要求不能修改原数组。
3. 最优解法:快慢指针法
3.1 算法思路
这道题的最优解法是将数组视为链表,使用快慢指针来检测环。具体思路如下:
- 将数组索引和值看作链表节点和指针
- 因为有重复数字,所以必然存在环
- 使用快慢指针找到环的入口点,即为重复数字
3.2 详细步骤
-
初始化快慢指针:
- 慢指针:slow = nums[0]
- 快指针:fast = nums[nums[0]]
-
第一阶段:找到相遇点
- 移动慢指针一步:slow = nums[slow]
- 移动快指针两步:fast = nums[nums[fast]]
- 重复直到slow == fast
-
第二阶段:找到环入口
- 重置慢指针:slow = nums[0]
- 同时移动快慢指针一步,直到相遇
- 相遇点即为重复数字
3.3 代码实现
python复制def findDuplicate(nums):
# 第一阶段:找到相遇点
slow = nums[0]
fast = nums[nums[0]]
while slow != fast:
slow = nums[slow]
fast = nums[nums[fast]]
# 第二阶段:找到环入口
slow = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
3.4 复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
完全符合题目所有要求。
4. 数学原理详解
4.1 为什么可以看作链表
将数组索引和值看作链表节点和指针:
- 每个节点的值是下一个节点的索引
- 因为有重复数字,所以至少有两个节点指向同一个节点
- 这就形成了环
4.2 为什么快慢指针能找到环
根据Floyd判圈算法:
- 快指针每次移动两步,慢指针每次移动一步
- 如果存在环,它们必定会在环内某点相遇
- 然后重置一个指针,以相同速度移动,再次相遇点就是环的入口
4.3 为什么环入口就是重复数字
因为环入口就是有两个指针指向它的节点,即有两个数组元素的值相同,也就是重复数字。
5. 边界条件与测试用例
5.1 常见测试用例
python复制# 示例1
nums = [1,3,4,2,2] # 输出:2
# 示例2
nums = [3,1,3,4,2] # 输出:3
# 边界情况
nums = [1,1] # 输出:1
nums = [1,1,2] # 输出:1
5.2 特殊边界处理
- 数组长度为2时:[1,1] → 直接返回1
- 多个重复数字时:返回任意一个即可
- 最大n情况:需要保证算法效率
6. 实际应用场景
这种算法虽然看起来是纯数学问题,但实际上有很多实际应用:
- 数据库索引检查:检测是否有重复索引
- 内存管理:检测内存地址是否重复分配
- 网络路由:检测路由循环
- 编译器优化:检测代码中的循环引用
7. 常见错误与调试技巧
7.1 常见错误
-
初始指针设置错误:
- 错误:slow = 0, fast = 0
- 正确:slow = nums[0], fast = nums[nums[0]]
-
移动指针时顺序错误:
- 必须先移动慢指针,再移动快指针
-
第二阶段忘记重置指针:
- 必须将slow重置为nums[0]
7.2 调试技巧
- 在小数组上手动模拟指针移动
- 打印每次移动后的指针位置
- 绘制链表结构图辅助理解
8. 算法优化与变种
8.1 二分查找解法
虽然快慢指针是最优解,但也可以使用二分查找:
- 在1到n之间进行二分
- 统计小于等于mid的数的个数
- 如果个数大于mid,则重复数在左半部分
python复制def findDuplicate(nums):
left, right = 1, len(nums)-1
while left < right:
mid = (left + right) // 2
count = sum(1 for num in nums if num <= mid)
if count > mid:
right = mid
else:
left = mid + 1
return left
时间复杂度O(nlogn),空间复杂度O(1),虽然不如快慢指针高效,但也是可行的解法。
8.2 位运算解法
还可以使用位运算,比较每一位上1的个数:
- 计算1到n的每一位1的总数
- 计算数组中每一位1的总数
- 如果某一位数组中的1比预期的多,则重复数该位为1
这种方法时间复杂度O(nlogn),空间复杂度O(1),实现较为复杂。
9. 同类题目推荐
掌握了这道题的解法后,可以尝试以下类似题目:
- LeetCode 141 - 环形链表
- LeetCode 142 - 环形链表 II
- LeetCode 268 - 丢失的数字
- LeetCode 448 - 找到所有数组中消失的数字
- LeetCode 645 - 错误的集合
10. 个人解题心得
在实际解决这个问题时,我有以下几点体会:
- 遇到数组找重复的问题,首先考虑哈希表,但要注意空间限制
- 当有严格空间限制时,快慢指针是非常有用的技巧
- 将数组视为链表是一个巧妙的思维转换
- 手动模拟小例子对理解算法很有帮助
- 二分查找虽然效率稍低,但在面试中也是可以接受的解法
这道题的关键在于突破常规思维,将数组索引和值的关系转化为链表结构,从而应用Floyd判圈算法。这种思维方式在解决其他算法问题时也非常有用。
