【LeetCode 热题100道笔记】寻找重复数

题目描述

给定一个包含 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)额外空间要求),但时间复杂度受排序影响。

算法过程

  1. 排序数组:对输入数组nums进行升序排序(排序后相同元素会相邻,便于后续查找)。
  2. 遍历查找重复值:从索引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)额外空间”要求。

算法过程(二分查找法)

  1. 初始化边界

    • 左边界 l = 1(元素最小值),右边界 r = n-1(元素最大值,因数组长度为 n+1),ans 存储最终重复值(初始为 -1)。
  2. 二分查找循环(当 l <= r 时):

    • 计算中间值 mid = (l + r) >> 1(等价于 Math.floor((l+r)/2),位运算更高效)。
    • 统计数组中 小于等于 mid 的元素个数 cnt(核心判断依据)。
    • 对比 cntmid
      • cnt <= mid:说明重复值不在 [1, mid] 范围内(因该区间元素个数正常,无多余),更新 l = mid + 1,继续查找右半区。
      • cnt > mid:说明重复值在 [1, mid] 范围内(该区间元素个数超标,存在多余),更新 r = mid - 1,并将 ans = mid(暂存当前可能的重复值,后续进一步缩小范围)。
  3. 返回结果

    • 循环结束后,ans 即为唯一重复值(因每次缩小范围时,仅当 cnt > mid 才更新 ans,最终会定位到最小的、满足 cnt > mid 的值,即重复值)。

时空复杂度分析(二分查找法)

维度复杂度说明
时间复杂度O(n log n)二分查找的次数为 O(log n)(范围从 1n,共 log2(n) 轮);每轮需遍历数组统计 cnt,耗时 O(n);总时间为 O(n × log n)。
空间复杂度O(1)仅使用 lrmidcntans 5个变量,无额外依赖与数组长度相关的数据结构,完全满足“常量级额外空间”要求。

核心逻辑解析

  1. 单调性依据
    对于 i < target,小于等于 i 的元素均为唯一,故 cnt = i;对于 i >= target,因 target 重复,小于等于 i 的元素个数 cnt = i + 1(或更多,若 target 重复多次),这一单调性确保二分查找的有效性。

  2. 为何 ans 最终是重复值
    每次 cnt > mid 时,mid 都可能是重复值,但需进一步缩小范围(更新 r = mid - 1),直到找到最小的满足 cnt > midmid——这个值就是重复值(因比它小的数均满足 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)空间+不修改数组”的所有要求,是最优解。

算法过程

  1. 阶段一:找环内相遇点

    • 初始化慢指针slow和快指针fast,均从nums[0]出发(因nums[0]的值在[1,n],必然指向环内或环入口)。
    • 慢指针每次移动1步(slow = nums[slow]),快指针每次移动2步(fast = nums[nums[fast]])。
    • 持续移动直到slow === fast,此时两指针在环内相遇(因环存在,必然相遇)。
  2. 阶段二:找环的入口(重复值)

    • 将慢指针slow重置为起点nums[0],快指针fast保持在相遇点不动。
    • 两指针同时每次移动1步(slow = nums[slow]fast = nums[fast])。
    • slow === fast时,相遇点即为环的入口,该值就是数组中唯一的重复值。

时空复杂度分析(快慢指针法)

维度复杂度说明
时间复杂度O(n)阶段一:快慢指针相遇最多遍历n步(环的长度不超过n);阶段二:找入口最多遍历n步;总时间为线性级O(n),满足进阶要求。
空间复杂度O(1)仅使用slowfast两个指针变量,无任何额外空间开销,完美符合“常量级额外空间”和“不修改数组”的约束。

核心逻辑解析(为何入口是重复值)

  1. 环的形成必然性
    数组有n+1个元素,值范围为[1,n],根据鸽巢原理,必存在重复值targettarget至少被两个不同索引指向(如i≠jnums[i]=nums[j]=target),故映射从ij都会指向target,形成以target为入口的环。

  2. 相遇点到入口的距离等于起点到入口的距离
    设起点到入口距离为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; // 入口即为重复值
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值