Leetcode-287-寻找重复数

题目描述

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

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

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

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

思路解析

怎样证明 nums 中存在至少一个重复值?其实很简单,这是“抽屉原理”(或者叫“鸽子洞原理”)的简单应用。
这里,nums 中的每个数字(n+1个)都是一个物品,nums 中可以出现的每个不同的数字(n个)都是一个 “抽屉”。把n+1 个物品放入n个抽屉中,必然至少会有一个抽屉放了2个或者2个以上的物品。所以这意味着nums中至少有一个数是重复的。

保存元素法

public int findDuplicate2(int[] nums){
    Set<Integer> set = new HashSet<>();
    for( Integer num : nums ){
        if( set.contains(num) )
            return num;
        else
            set.add(num);
    }
    return -1;
}

复杂度分析
时间复杂度:O(n),我们只对数组做了一次遍历,在HashMap和HashSet中查找的复杂度是O(1)。
空间复杂度:O(n),我们需要一个HashMap或者HashSet来做额外存储,最坏情况下,这需要线性的存储空间。
尽管时间复杂度较小,但以上两种保存元素的方法,都用到了额外的存储空间,这个空间复杂度不能让我们满意。

方法二 二分查找法
这道题目中数组其实是很特殊的,我们可以从原始的 [1, N] 的自然数序列开始想。现在增加到了N+1个数,根据抽屉原理,肯定会有重复数。对于增加重复数的方式,整体应该有两种可能:

  1. 如果重复数(比如叫做target)只出现两次,那么其实就是1~N所有数都出现了一次,然后再加一个target;
  2. 如果重复数target出现多次,那在情况1的基础上,它每多出现一次,就会导致1~N中的其它数少一个。
    例如:1~9之间的10个数的数组,重复数是6:
    1,2,5,6,6,6,6,6,7,9
    本来最简单(重复数出现两次,其它1~9的数都出现一次)的是
    1,2,3,4,5,6,6,7,8,9
    现在没有3、4和8,所以6会多出现3次。

我们可以发现一个规律:

  1. 以target为界,对于比target小的数i,数组中所有小于等于它的数,最多出现一次(有可能被多出现的target占用了),所以总个数不会超过i。
  2. 对于比target大的数j,如果每个元素都只出现一次,那么所有小于等于它的元素是j个;而现在target会重复出现,所以总数一定会大于j。

用数学化的语言描述就是:
我们把对于1~N内的某个数i,在数组中小于等于它的所有元素的个数,记为count[i]。
则:当i属于[1, target-1]范围内,count[i] <= i;当i属于[target, N]范围内,count[i] > i。

所以要找target,其实就是要找1N中这个分界的数。所以我们可以对1N的N个自然数进行二分查找,它们可以看作一个排好序的数组,但不占用额外的空间。

public int findDuplicate3(int[] nums){
    int l = 1;
    int r = nums.length - 1;
    // 二分查找
//1,2,5,6,6,6,6,6,7,9

    while (l <= r){
        int i = (l + r) / 2; 
        // 对当前i计算count[i]
        int count = 0;
        for( int j = 0; j < nums.length; j++ ){
            if (nums[j] <= i)
                count ++;
        }
        // 判断count[i]和i的大小关系
        if ( count <= i )
            l = i + 1; 
        else
            r = i; 
        // 找到target
        if (l == r)
            return l;
    }
    return -1;
}

复杂度分析
时间复杂度:O(nlog n),其中 n 为nums[] 数组的长度。二分查找最多需要O(logn) 次,而每次判断count的时候需要O(n) 遍历 nums[] 数组求解小于等于 i 的数的个数,因此总时间复杂度为O(nlogn)。
空间复杂度:O(1)。我们只需要常数空间存放若干变量。

方法三 排序法

另一个想法是,我们可以先在原数组上排序。
排序之后,所有重复的数会排在一起;这样,只要我们遍历的时候发现连续两个元素相等,就可以输出结果了。

public int findDuplicate3(int[] nums){
    Arrays.sort(nums);
    for( int i = 1; i < nums.length; i++ ){
        if( nums[i] == nums[i-1] )
            return nums[i];
    }
    return -1;
}

复杂度分析
时间复杂度: O(nlgn)。对数组排序,在Java 中要花费 O(nlgn) 时间,后续是一个线性扫描,所以总的时间复杂度是O(nlgn)。

空间复杂度: O(1) (or O(n)),在这里,我们对 nums 进行了排序,因此内存大小是固定的。当然,这里的前提是我们可以用常数的空间,在原数组上直接排序。如果我们不能修改输入数组,那么我们必须把 nums 拷贝出来,并进行排序,这需要分配线性的额外空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值