「前端刷题」287.寻找重复数(MEDIUM)

45 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第25天,点击查看活动详情

题目(Find the Duplicate Number)

链接:https://leetcode-cn.com/problems/find-the-duplicate-number
解决数:1807
通过率:64.6%
标签:位运算 数组 双指针 二分查找 
相关公司:amazon microsoft google 

给定一个包含 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

 

提示:

  • 1 <= n <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

 

进阶:

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

思路

  • 数组只读——不能去移动数组,不能排序
  • 常量空间——不能使用哈希表,不能新建数组再排序
  • 时间复杂度小于O(N2)O(N^2)——不能用暴力法
  • 只有一个数字重复,但可能重复多次

二分查找除了对索引二分,还有值域二分

  • 数组元素是 1 - n 中的某一个,出现的位置不确定,但值域是确定的。

    1. 对索引二分,一般用于有序数组中找元素,因为索引的大小可以反映值的大小,因此对索引二分即可。
    2. 对值域二分。重复数落在 [1, n] ,可以对 [1, n] 这个值域二分查找。
  • mid = (1 + n) / 2,重复数要么落在[1, mid],要么落在[mid + 1, n]

  • 遍历原数组,统计 <= mid 的元素个数,记为 k。

  • 如果k > mid,说明有超过 mid 个数落在[1, mid],但该区间只有 mid 个“坑”,说明重复的数落在[1, mid]

  • 相反,如果k <= mid,则说明重复数落在[mid + 1, n]

  • 对重复数所在的区间继续二分,直到区间闭合,重复数就找到了。

时间复杂度:二分法O(logN)O(logN),但二分法内部遍历了一次数组O(N)O(N),综合为O(NlogN)O(NlogN) 空间复杂度:O(1)O(1)

const findDuplicate = (nums) => {
    let lo = 1;
    let hi = nums.length - 1; //题目注明了:nums.length == n + 1
    while (lo < hi) {
        const mid = (lo + hi) >>> 1;  // 求中间索引
        let count = 0;
        for (let i = 0; i < nums.length; i++) {
            if (nums[i] <= mid) {
                count++;
            }
        }
        if (count > mid) {
            hi = mid;
        } else {
            lo = mid + 1;
        }
    }
    return lo;
};

快慢指针法

  • 分析这个数组,索引从 0n0~n ,值域是 1n1~n 。值域,在索引的范围内,值可以当索引使。
  • 比如,nums 数组:[4,3,1,2,2][4, 3, 1, 2, 2]
  • 以 nums[0] 的值 4 作为索引,去到 nums[4]
  • 以 nums[4] 的值 2 作为索引,去到 nums[2]
  • 以 nums[2] 的值 1 作为索引,去到 nums[1]……
  • 从一项指向另一项,将nums数组抽象为链表:4->2->1->3->2,如下图,链表有环。 微信截图_20200526201809.png

有环链表,重复数就是入环口

  • 题目说数组必存在重复数,所以 nums 数组肯定可以抽象为有环链表。
  • 题目转为:求该有环链表的入环口。因为入环口的元素就是重复的链表节点值。
  • 怎么求入环口?可参考我另一篇大白话题解 有环链表怎么求入环口

快慢指针法 代码

const findDuplicate = (nums) => {
    let slow = 0;
    let fast = 0;
    while (true) {
        slow = nums[slow];
        fast = nums[nums[fast]];   // slow跳一步,fast跳两步
        if (slow == fast) {        // 指针首次相遇
            fast = 0;              // 让快指针回到起点
            while (true) {         // 开启新的循环
                if (slow == fast) {// 如果再次相遇,就肯定是在入口处
                    return slow;   // 返回入口,即重复的数
                }
                slow = nums[slow]; // 两个指针每次都进1步
                fast = nums[fast];
            }
        }
    }
};