今天分享的题目来源于 LeetCode 第 287 号问题:寻找重复数。

题目描述

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

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

示例 2:

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

说明:

  • 不能更改原数组(假设数组是只读的)。
  • 只能使用额外的 O(1) 的空间。
  • 时间复杂度小于 O(n2) 。
  • 数组中只有一个重复的数字,但它可能不止重复出现一次。

题目解析

个人博客:www.cxyxiaowu.com

给定一个整形数组,数组的长度是 n + 1,数组里面存放的元素是区间 [1, n] 上的数,这个数组中有且仅有一个元素是重复的,题目让我们找出这个元素,并且这道题给了如下的限制:

  • 不能改变数组
  • 只能使用 O(1) 的空间
  • 时间复杂度必须小于 O(n^2)
  • 重复的元素可重复多次

首先不能改变数组导致无法排序,也无法用 index 和元素建立关系;

只能使用 O(1) 的空间意味着使用哈希表去计数这条路也走不通;

时间复杂度必须小于 O(n^2) 表示暴力求解也不行;

重复的元素可重复多次 这一条加上后,本来可以通过累加求和然后做差 sum(array) - sum(1,2,...,n) 的方式也变得不可行。

本来是非常简单的一道数组遍历的题目,加上了上面这四个条件后感觉有点无从下手

我们说做题要借助算法,而不是空想,因此对于这道题也不例外,我们可以问自己这样一个问题,就是 “什么样的算法可以不使用额外的空间解决数组上面的搜索问题?”

静静的思索一下。

你这时应该隐隐约约知道了,难道是 二分查找

什么?二分查找法?!

二分查找法不是对有序数组才适用么?

这里澄清一个误区,二分法的使用 并不一定 需要在排序好的数组上面进行,不要让常见的例题限制了你的思路,二分法还有一个比较高级的用法叫做 按值二分

这道题目交代的信息很少,我们只需要关注两个东西 - 数组,数组里的元素,利用二分我们需要去思考的是,我们要找符合条件的元素作为答案,那么比答案小的元素具有什么样的特质,比答案大的元素又具有什么样的特质?,结合题目给我们的例子来看看:

( **说明:**下面的 <= 符号表明 小于或者等于。)

例1:

[1,3,4,2,2]                         元素个数
<= 1 的元素:1                          1
<= 2 的元素:1, 2, 2                    3
<= 3 的元素:1, 2, 2, 3                 4
<= 4 的元素:1, 2, 2, 3, 4              5

例2:

[3,1,3,4,2]
<= 1 的元素:1                          1
<= 2 的元素:1, 2                       2
<= 3 的元素:1, 2, 3, 3                 4
<= 4 的元素:1, 2, 3, 3, 4              5

极端一点的例子 (必须保证数组的长度是 n + 1, 并且元素都在区间[1,n] 上, 有且只有一个重复)

[3,3,3,3,4]
<= 1 的元素:                           0
<= 2 的元素:                           0
<= 3 的元素:3, 3, 3, 3                 4
<= 4 的元素:3, 3, 3, 3, 4              5

看完上面几个例子,相信你明白了一个事实:

  • “如果选中的数 小于 我们要找的答案,那么整个数组中小于或等于该数的元素个数必然小于或等于该元素的值;

  • 如果选中的数 大于或等于 我们要找的答案,那么整个数组中小于或等于该数的元素个数必然 大于 该元素的值”

而且你可以看到,我们要找的答案其实就处于一个分界点的位置,寻找边界值,这又是二分的一个应用,而且题目已经告诉我们数组里面的值只可能在 [1, n] 之间,这么一来,思路就是在 [1, n] 区间上做二分,然后按我们之前提到的逻辑去做分割。整个解法的时间复杂度是 O(nlogn),也是满足题目要求的。

上面的解法不是最优的,但是个人觉得是根据现有的知识比较容易想到的。

另外一种 O(n) 的解法借鉴快慢指针找交点的思想,算法非常的巧妙,也非常的有趣,但不太容易想到,这里把代码放上。

代码实现一

//二分查找
class Solution {
    public int findDuplicate(int[] nums) {
         int len = nums.length;
        int start = 1;
        int end = len - 1;

        while (start < end) {
            int mid = start + (end - start) / 2;
            int counter = 0;
            for (int num:nums) {
                if (num <= mid) {
                    counter++;
                }
            }
            if (counter > mid) {
                end = mid;
            } else {
                start = mid + 1;
            }
        }
        return start;
    }
}

代码实现二

//快慢指针
public int findDuplicate(int[] nums) {
    int fast = nums[nums[0]];
    int slow = nums[0];

    while (fast != slow) {
        fast = nums[nums[fast]];
        slow = nums[slow];
    }

    slow = 0;
    while (fast != slow) {
        fast = nums[fast];
        slow = nums[slow];
    }

    return slow;
}

❤️ 看完三件事:

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个忙:

  • 点赞,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
  • 关注我和专栏,让我们成为长期关系
  • 关注公众号「五分钟学算法」,第一时间阅读最新的算法文章,公众号后台回复 1024 送你 50 本 算法编程书籍。
10-20 00:21