题目描述
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
示例 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<=1051 <= n <= 10^51<=n<=105
- nums.length == n + 1
- 1 <= nums[i] <= n
- nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
进阶:
如何证明 nums 中至少存在一个重复的数字?
你可以设计一个线性级时间复杂度 O(n)O(n)O(n) 的解决方案吗?
思考一
基于“鸽巢原理”(n+1个元素放入n个“鸽巢”,必存在重复),先通过排序将相同元素聚集,再遍历数组找到连续相等的元素,该元素即为唯一重复值。此方法无需修改原数组(排序会生成临时空间,而非修改输入数组本身),且额外空间仅为排序所需的常数级(取决于排序实现,本题代码满足O(1)额外空间要求),但时间复杂度受排序影响。
算法过程
- 排序数组:对输入数组
nums进行升序排序(排序后相同元素会相邻,便于后续查找)。 - 遍历查找重复值:从索引1开始遍历排序后的数组,比较当前元素
nums[i]与前一个元素nums[i-1]:- 若两者相等,说明找到唯一重复值,直接返回该元素。
- 因题目明确“只有一个重复整数”,无需遍历完整数组,找到后即可终止。
时空复杂度分析
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | 核心耗时为排序操作(主流排序算法如快排、归并排序的时间复杂度为O(n log n)),遍历过程为O(n),整体由排序主导。 |
| 空间复杂度 | O(1) 或 O(n) | 若使用原地排序(如快排),额外空间为O(1)(满足题目“常量级额外空间”要求);若排序需额外数组(如归并排序),则为O(n)。本题JavaScript的sort()方法在不同引擎实现中多为原地优化,可认为满足O(1)额外空间。 |
代码
/**
* @param {number[]} nums
* @return {number}
*/
var findDuplicate = function(nums) {
nums.sort();
for (let i = 1; i < nums.length; i++) {
if (nums[i] === nums[i-1]) return nums[i];
}
};
思考二:二分查找
基于“数值范围与计数的单调性”设计:数组元素范围固定为 [1, n],若不存在重复值,小于等于 i 的元素个数必等于 i;若存在重复值 target,则小于等于 target 的元素个数必大于 target(因 target 至少多出现一次)。利用这一特性,通过二分法缩小范围,最终定位到唯一的 target,满足“不修改数组+O(1)额外空间”要求。
算法过程(二分查找法)
-
初始化边界:
- 左边界
l = 1(元素最小值),右边界r = n-1(元素最大值,因数组长度为n+1),ans存储最终重复值(初始为-1)。
- 左边界
-
二分查找循环(当
l <= r时):- 计算中间值
mid = (l + r) >> 1(等价于Math.floor((l+r)/2),位运算更高效)。 - 统计数组中 小于等于
mid的元素个数cnt(核心判断依据)。 - 对比
cnt与mid:- 若
cnt <= mid:说明重复值不在[1, mid]范围内(因该区间元素个数正常,无多余),更新l = mid + 1,继续查找右半区。 - 若
cnt > mid:说明重复值在[1, mid]范围内(该区间元素个数超标,存在多余),更新r = mid - 1,并将ans = mid(暂存当前可能的重复值,后续进一步缩小范围)。
- 若
- 计算中间值
-
返回结果:
- 循环结束后,
ans即为唯一重复值(因每次缩小范围时,仅当cnt > mid才更新ans,最终会定位到最小的、满足cnt > mid的值,即重复值)。
- 循环结束后,
时空复杂度分析(二分查找法)
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n log n) | 二分查找的次数为 O(log n)(范围从 1 到 n,共 log2(n) 轮);每轮需遍历数组统计 cnt,耗时 O(n);总时间为 O(n × log n)。 |
| 空间复杂度 | O(1) | 仅使用 l、r、mid、cnt、ans 5个变量,无额外依赖与数组长度相关的数据结构,完全满足“常量级额外空间”要求。 |
核心逻辑解析
-
单调性依据:
对于i < target,小于等于i的元素均为唯一,故cnt = i;对于i >= target,因target重复,小于等于i的元素个数cnt = i + 1(或更多,若target重复多次),这一单调性确保二分查找的有效性。 -
为何
ans最终是重复值:
每次cnt > mid时,mid都可能是重复值,但需进一步缩小范围(更新r = mid - 1),直到找到最小的满足cnt > mid的mid——这个值就是重复值(因比它小的数均满足cnt = mid,无重复;它本身满足cnt > mid,存在重复)。
代码
/**
* @param {number[]} nums
* @return {number}
*/
var findDuplicate = function(nums) {
const n = nums.length;
let l = 1, r = n - 1, ans = -1;
while (l <= r) {
let mid = (l + r) >> 1;
let cnt = 0;
for (let i = 0; i < n; i++) {
cnt += nums[i] <= mid;
}
if (cnt <= mid) {
l = mid + 1;
} else {
r = mid - 1;
ans = mid;
}
}
return ans;
};
思考三:快慢指针
将数组视为“索引→值”的映射函数(f(i) = nums[i]),由于存在重复值且元素范围为[1,n],映射必然会形成环形环(重复值是环的入口,被多个索引指向)。利用Floyd判圈算法,分两步找到环的入口:先用快慢指针确定环的存在(找到相遇点),再通过双指针同步移动定位入口,该入口即为唯一重复值。此方法满足“O(n)时间+O(1)空间+不修改数组”的所有要求,是最优解。
算法过程
-
阶段一:找环内相遇点
- 初始化慢指针
slow和快指针fast,均从nums[0]出发(因nums[0]的值在[1,n],必然指向环内或环入口)。 - 慢指针每次移动1步(
slow = nums[slow]),快指针每次移动2步(fast = nums[nums[fast]])。 - 持续移动直到
slow === fast,此时两指针在环内相遇(因环存在,必然相遇)。
- 初始化慢指针
-
阶段二:找环的入口(重复值)
- 将慢指针
slow重置为起点nums[0],快指针fast保持在相遇点不动。 - 两指针同时每次移动1步(
slow = nums[slow],fast = nums[fast])。 - 当
slow === fast时,相遇点即为环的入口,该值就是数组中唯一的重复值。
- 将慢指针
时空复杂度分析(快慢指针法)
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 阶段一:快慢指针相遇最多遍历n步(环的长度不超过n);阶段二:找入口最多遍历n步;总时间为线性级O(n),满足进阶要求。 |
| 空间复杂度 | O(1) | 仅使用slow和fast两个指针变量,无任何额外空间开销,完美符合“常量级额外空间”和“不修改数组”的约束。 |
核心逻辑解析(为何入口是重复值)
-
环的形成必然性:
数组有n+1个元素,值范围为[1,n],根据鸽巢原理,必存在重复值target。target至少被两个不同索引指向(如i≠j但nums[i]=nums[j]=target),故映射从i和j都会指向target,形成以target为入口的环。 -
相遇点到入口的距离等于起点到入口的距离:
设起点到入口距离为a,入口到相遇点距离为b,环长为c。根据快慢指针速度关系(快=2×慢),可推导出a = (k×c - b)(k为整数),即起点到入口的距离等于相遇点绕环k圈后到入口的距离。因此,两指针同步移动时必在入口相遇。
代码
var findDuplicate = function(nums) {
// 1. 快慢指针找环(相遇点)
let slow = nums[0], fast = nums[0];
do {
slow = nums[slow]; // 慢指针走1步
fast = nums[nums[fast]]; // 快指针走2步
} while (slow !== fast);
// 2. 找环的入口(重复值)
slow = nums[0]; // 慢指针回到起点
while (slow !== fast) {
slow = nums[slow]; // 快慢指针均走1步
fast = nums[fast];
}
return slow; // 入口即为重复值
};
821

被折叠的 条评论
为什么被折叠?



