题目描述
给定一个包含 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个数,根据抽屉原理,肯定会有重复数。对于增加重复数的方式,整体应该有两种可能:
- 如果重复数(比如叫做target)只出现两次,那么其实就是1~N所有数都出现了一次,然后再加一个target;
- 如果重复数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次。
我们可以发现一个规律:
- 以target为界,对于比target小的数i,数组中所有小于等于它的数,最多出现一次(有可能被多出现的target占用了),所以总个数不会超过i。
- 对于比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 拷贝出来,并进行排序,这需要分配线性的额外空间。