287. 寻找重复数
287. 寻找重复数
题目链接(中等)
题目描述
给定一个包含 n + 1 个整数的数组
nums,其数字都在 [1, n] 范围内(包括
1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数,返回
这个重复的数。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1) 的额外空间。
数据范围:
1 <= n <= 10^5nums.length == n + 11 <= nums[i] <= nnums中 只有一个整数 出现 两次或多次,其余整数均只出现 一次
进阶:
- 如何证明
nums中至少存在一个重复的数字? - 你可以设计一个线性级时间复杂度
O(n)的解决方案吗?
示例
示例 1:
输入: nums = [1,3,4,2,2]
输出: 2
示例 2:
输入: nums = [3,1,3,4,2]
输出: 3
示例 3:
输入: nums = [3,3,3,3,3]
输出: 3
核心思路
本题的限制很苛刻:
- 不能修改数组;
- 只能用 额外空间;
- 进阶要求 时间。
朴素解法:
- 排序后相邻比较:,但修改了数组,不符合;
- 哈希集合: 时间,但 空间,不符合。
三种符合要求的解法:
- 二分查找:在值域
[1, n]上二分答案,利用cnt[i]的单调性, 时间、 空间; - 二进制按位统计:逐位比较
nums和[1, n]中该位为 1 的个数, 时间、 空间; - 快慢指针(Floyd 判圈):把数组视为链表,找环的入口, 时间、 空间,满足进阶要求。
方法一:二分查找(在值域上二分)
思路及解法
定义 cnt[i] = nums 中小于等于
i 的数的个数。
关键性质:设重复数为 target,则:
- 当
i < target时,cnt[i] ≤ i; - 当
i ≥ target时,cnt[i] > i。
为什么?
- 数组共有
n + 1个数,取值范围[1, n]; - 如果每个数只出现一次,那么
cnt[i] = i; - 现在有一个数
target多出现了(至少一次),那么从target开始,cnt[i]会比i多至少 1。
所以 cnt[i] 随 i
增大具有单调性,可以用二分查找找到第一个满足
cnt[i] > i 的 i,就是答案。
举例:nums = [1,3,4,2,2],n = 4
i |
cnt[i](≤ i 的个数) |
cnt[i] 与 i 比较 |
|---|---|---|
| 1 | 1 | ≤ |
| 2 | 3 | > |
| 3 | 4 | > |
| 4 | 5 | > |
第一个 cnt[i] > i 的 i = 2,即答案。
二分过程:
- 在
[1, n]中二分mid; - 统计
nums中≤ mid的个数cnt; - 若
cnt ≤ mid,说明target > mid,l = mid + 1; - 否则
target ≤ mid,记录答案并r = mid - 1。
代码
class Solution:
def findDuplicate(self, nums: list[int]) -> int:
n = len(nums)
l, r = 1, n - 1
ans = -1
while l <= r:
mid = (l + r) // 2
cnt = sum(1 for x in nums if x <= mid)
if cnt <= mid:
l = mid + 1
else:
r = mid - 1
ans = mid
return ans复杂度分析
- 时间复杂度:,二分 次,每次统计 。
- 空间复杂度:,只使用常数个变量。
方法二:二进制按位统计
思路及解法
逐位确定重复数的每一位。
关键观察:设 nums 中第 i
位为 1 的个数为 x,数字 [1, n] 中第
i 位为 1 的个数为 y,则:
- 若
target第i位为 1,则x > y; - 若
target第i位为 0,则x ≤ y。
为什么?
假设 target 出现两次,其余数各出现一次:
target第i位为 1 →x比y大 1;target第i位为 0 →x = y。
若 target 出现多次,则某些数字被替换掉:
- 若被替换的数该位为 1、
target该位为 1 →x不变,仍> y; - 若被替换的数该位为 0、
target该位为 1 →x增加,仍> y; - 若被替换的数该位为 1、
target该位为 0 →x减少,仍≤ y; - 若被替换的数该位为 0、
target该位为 0 →x不变,仍≤ y。
因此逐位判断 x > y 即可还原 target。
代码
class Solution:
def findDuplicate(self, nums: list[int]) -> int:
n = len(nums) - 1 # 值域上限
bit_max = (n - 1).bit_length() # 最大位数
ans = 0
for bit in range(bit_max):
x = y = 0
for i, v in enumerate(nums):
if v & (1 << bit):
x += 1
for i in range(1, n + 1):
if i & (1 << bit):
y += 1
if x > y:
ans |= 1 << bit
return ans复杂度分析
- 时间复杂度:,枚举 位,每位遍历 。
- 空间复杂度:。
方法三:快慢指针(Floyd 判圈)✅ 推荐
思路及解法
关键转化:把数组 nums
视为一个链表,位置 i 连一条
i → nums[i] 的边。
由于存在重复数字
target,至少有两个不同的位置指向同一个位置
target,所以这个「链表」中一定存在环,且环的入口就是
target。
这样本题就等价于 LC 142. 环形链表 II:找环的入口。
Floyd 判圈算法:
- 慢指针
slow每次走一步,快指针fast每次走两步; - 两者一定会在环内相遇;
- 相遇后,让
slow重置为起点0,与fast同速前进; - 它们会在环的入口相遇,即答案。
为什么这样能找到环的入口?
设:
- = 起点到环入口的距离;
- = 环入口到相遇点的距离;
- = 相遇点回到环入口的距离;
- 为环长。
相遇时:
slow走了 步;fast走了 步(多绕了 圈)。
化简:,即 。
所以从起点走 步和从相遇点走 (可能再加几圈)会同时到达环入口,两者必在环入口相遇。
代码
class Solution:
def findDuplicate(self, nums: list[int]) -> int:
# 第一步:快慢指针找相遇点
slow = fast = 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# 第二步:一个从头出发,一个从相遇点出发,同速前进
slow = 0
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow复杂度分析
- 时间复杂度:,Floyd 判圈算法是线性的。
- 空间复杂度:,只用两个指针。
三种方法对比
| 方法 | 时间 | 空间 | 是否满足进阶 | 特点 |
|---|---|---|---|---|
| 二分查找 | ❌ | 思路巧妙,但时间不是线性 | ||
| 二进制按位 | ❌ | 按位还原,比较「偏门」 | ||
| 快慢指针 | ✅ | 最优解,也最优雅 |
推荐:
- 面试:首选快慢指针,时间 、空间 ,满足进阶要求;
- 开场思路:可以先讲二分,因为它更容易想到;
- 加分:能讲清「把数组视为链表,找环入口」这个转化,非常加分。
关键细节
1. 二分法的单调性
cnt[i] 随 i
增大是单调不减的,且满足:
i < target时cnt[i] ≤ i;i ≥ target时cnt[i] > i。
所以可以用二分找第一个 cnt[i] > i
的位置。
2. 快慢指针为什么不会死循环
因为数组中一定存在环(由题目保证:n + 1
个位置、n 个值、至少一个重复)。Floyd
判圈算法保证在有环时一定能相遇。
3. 快慢指针的初始化
本题不能用 slow = fast = head +
先比较的写法(会导致初始即相等):
slow = fast = 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break因为起点 0 和 nums[0]
逻辑上不相同,所以先移动再比较。用
while True + break 是最清晰的写法。
4. 为什么「不修改数组」
- 排序会修改数组;
- 快慢指针只读取
nums[i],不写; - 二分只统计个数,不写;
- 二进制只统计各位,不写。
三种方法都满足「不修改数组」。
5. 与 LC 142 环形链表 II 的关系
本题的解法就是 LC 142 的完全套用:
- 数组下标 = 链表节点;
nums[i]=next指针;- 重复数字 = 环的入口。
如果对 LC 142 熟悉,本题可以直接秒。
总结
- 本质:
n + 1个数填在n个位置,一定有一个重复数; - 二分:
- 在值域
[1, n]上二分; - 利用
cnt[i]的单调性找第一个cnt[i] > i; - 、 空间;
- 在值域
- 二进制按位:逐位比较
nums和[1, n]中该位 1 的个数,; - 快慢指针(最优):
- 把数组视为链表:
i → nums[i]; - 重复数即环的入口;
- Floyd 判圈算法找入口,、;
- 把数组视为链表:
- 通用套路:「数组 + 重复数」问题,如果能容忍 ,用二分;要求 ,用快慢指针。
相关题目
- LC 141. 环形链表(判断是否有环)
- LC 142. 环形链表 II(找环的入口)
- LC 442. 数组中重复的数据(找所有重复数,可修改数组)
- LC 448. 找到所有数组中消失的数字(原地哈希)
- LC 剑指 Offer 03. 数组中重复的数字(可修改数组)