前端刷题路-Day88:寻找重复数(题号287)

734 阅读2分钟

这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战

寻找重复数(题号287)

题目

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1n 之间(包括 1n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,找出 这个重复的数

你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:

输入:nums = [1,3,4,2,2]
输出:2

示例 2:

输入:nums = [3,1,3,4,2]
输出:3

示例 3:

输入:nums = [1,1]
输出:1

示例 4:

输入:nums = [1,1,2]
输出:1

提示:

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

进阶:

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

链接

leetcode-cn.com/problems/fi…

解释

这题啊,这题是自闭草丛。

讲真,官方推荐的三种想法根本想不到,抛去第二种二进制不说,另外两种也是根本想不到。

题目给的例子很有迷惑性,让人觉得数字是连续的,只是顺序不一样罢了,这样的解法就很简单了,只要扫一次,拿到最大值和最小值,同时累计所有数字的和,然后用最大值、最小值计算除正常情况下的和,关于这一点在小学二年级就学过了。

最后用两个和的差值除以多出来的数字个数,就完事了,很简单就能拿到最后的结果。

但题目他*的不是这样的啊,别看例子上都是连续的数字,其实不然,有可能出现不连续的情况,比方说酱婶的:

[1, 4, 4, 2, 4]

如果是这种情况的话,显然是不能使用这种首尾元素求和的方式了。

笔者能想到的就只有利用Map来缓存出现过的数字了,如果遇到已经存在的数字返回即可。

这种做法虽然可以,但不符合空间复杂度O(1)的要求了,要不然这题也不会是中等难度。

那还有别的方法么?显然是有的,但是很难想到,笔者也是花了比较久的时间才理解。

首先是二分,二分?这又不是有序数组,为什么要用二分?

笔者也是一头雾水,后来发现这是针对值的二分,什么叫值的二分?

由于题目说了,数字必然是存在于[1, n]的区间里的,那么这样就可以累计数字的个数来实现二分操作。

怎么说?如果重复的数字出现在[1, x],那么在[1, x]这个区间内出现数字的个数肯定比x大,这样就可以利用数字个数不断缩小这个区间,最后缩小到一定程度,就是最后的答案了。

不过这个方案的时间复杂度是O(nlogn),仍然不是最优解。

最优解的二进制就不说了,笔者也不是很了解,重点在于双指针。

双指针这也能用笔者也是没想到的,这里的双指针并非是平常的用双指针来缩小一个区间,而是快慢指针。

快慢指针最经典的用法就是用来判断一个链表是不是环形链表,但这数组怎么能和环形链表产生关系呢?

还记得有一题是青蛙跳水吧,数组的每个元素代表元素能去到的位置,也就是数组的值代表着index,这题如果这么理解一个数组,就可以将数组想象成一个链表,重复的元素就是链表形成环的环开始节点。

快慢指针的解法这里不多赘述,有兴趣的同学可以点击这里查看详情。

自己的答案(hash

var findDuplicate = function(nums) {
  const set = new Set()
  for (let i = 0; i < nums.length; i++) {
    if (set.has(nums[i])) return nums[i]
    set.add(nums[i])
  }
};

解法简单,除了不符合题目规定外没啥缺点。

更好的方法(二分)

var findDuplicate = function(nums) {
  let left = 1
  let right = nums.length - 1
  while (left < right) {
    const mid = ~~((left + right) / 2)
    const sum = nums.reduce((count, item) => count + (item <= mid ? 1 : 0), 0)
    if (sum > mid) {
      right = mid
    } else {
      left = mid + 1
    }
  }
  return left
};

整体逻辑就是经典二分的逻辑,需要注意的可能就是sum的取值吧,需要累计数组中所有小于等于mid值的个数,其它没啥了,二分就完事了。

更好的方法(双指针)

var findDuplicate = function(nums) {
    let slow = 0, fast = 0;
    do {
        slow = nums[slow];
        fast = nums[nums[fast]];
    } while (slow != fast);
    slow = 0;
    while (slow != fast) {
        slow = nums[slow];
        fast = nums[fast];
    }
    return slow;
};

这里直接嫖了官方的答案,有兴趣的可以点击这里查看原答案。

官方很巧妙的用了do...while,要不是看见了,压根不记得JavaScript还有do...while这种东西。

do...while主要是因为快慢指针一开始就是重合的,如果使用while,循环压根不会开始,而do...while不管怎样都会执行一次,而执行完这一次之后快慢指针的指向就不一样了,等到它们第二次重合的时候就终止,开始慢指针的重置操作。

第二个while就比较正常了,也就是我们第二轮的比较,没啥可说的。



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)