题目描述:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解题思路:
解决这个问题最简单的办法是将输入的数组排序,从排序的数组中找出重复的数字只需要从头到尾扫描即可,所以先排序再查找的时间复杂度主要就取决于排序算法,一般为O(nlogn)。
还是那句话:“最容易想到的往往不是最佳解法”。
进一步考虑,我们也不难想到借助空间换时间的思路,通过哈希表来解决。从头到尾按顺序扫描整个数组,依次将其存入哈希表,每扫描到一个元素,都可以用O(1)的时间判断哈希表里是否已经存在该值,如果不存在,就将其加入哈希表,继续扫描下一个,如果存在,那么该元素就是第一个重复的数字。这个算法的时间复杂度是O(n),但很显然这个效率的提升也是用空间换来的。
因此,我们希望得到一种不消耗额外空间的算法,也就是本题的第三种解法:数组重排。由于题目中告诉我们所有的数字都在0到n-1的范围内,因此如果没有重复,那么所存储的值也正好是0到n-1这n个数字,我们把原数组重新排列为一个元素和对应下标值相同的数组。具体思路如下:
从头到尾扫描整个数组中的数字,当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于下标i,如果是,接着比较下一个数字;如果不是,则将其与第m个数字比较,若与第m个数字相同,则说明它就是一个重复数字,如果不同,就将其与第m个数字进行交换,也就是把它放到自己应在的位置去。重复这个过程,直到该位置上的数与下标相同为止。
该算法看起来是两层循环,但是每个数字最多进行两次交换就会找到属于自己的位置,因为总的时间复杂度还是O(n),不需要额外内存。
举例:
以{2,3,1,0,2,5,3}为例:
- 0(索引值)和2(索引值位置的元素)不相等,并且2(索引值位置的元素)和1(以该索引值位置的元素2为索引值的位置的元素)不相等,则交换位置,数组变为:{1,3,2,0,2,5,3};
- 0(索引值)和1(索引值位置的元素)仍然不相等,并且1(索引值位置的元素)和3(以该索引值位置的元素1为索引值的位置的元素)不相等,则交换位置,数组变为:{3,1,2,0,2,5,3};
- 0(索引值)和3(索引值位置的元素)仍然不相等,并且3(索引值位置的元素)和0(以该索引值位置的元素3为索引值的位置的元素)不相等,则交换位置,数组变为:{0,1,2,3,2,5,3};
- 0(索引值)和0(索引值位置的元素)相等,遍历下一个元素;
- 1(索引值)和1(索引值位置的元素)相等,遍历下一个元素;
- 2(索引值)和2(索引值位置的元素)相等,遍历下一个元素;
- 3(索引值)和3(索引值位置的元素)相等,遍历下一个元素;
- 4(索引值)和2(索引值位置的元素)不相等,但是2(索引值位置的元素)和2(以该索引值位置的元素2为索引值的位置的元素)相等,则找到了第一个重复的元素。
编程实现(Java):
public class Solution {
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
// 这里要特别注意~返回任意重复的一个,赋值duplication[0]
// Return value:true if the input is valid, and there are some duplications in the array number
// otherwise false
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(numbers==null || length<1)
return false;
//三种方法:排序后查找、哈希表
//第三种:数组重排
for(int i=0;i<length;i++){
while(numbers[i]!=i){ //每个元素最多被交换两次就可以找到自己的位置,依次复杂度是O(n)
if(numbers[numbers[i]]==numbers[i]){
duplication[0]=numbers[i];
return true;
}else{
int temp=numbers[numbers[i]]; //交换
numbers[numbers[i]]=numbers[i]; //将numbers[i]放到属于他的位置上
numbers[i]=temp;
}
}
}
return false;
}
补充说明:
(1) 如果只要求判断是否有重复元素,不用找到该值,那么可以使用异或的思路,所有的元素和从0到n-1的下标一起异或,那么如果没有重复元素,相当于从0到n-1每个元素都出现了两次(下标和对于的元素),最后的异或结果一定是0,否则说明有重复元素。
(2) 上述数组重排的思路虽然比较巧妙,但是一个缺点是改变了原来的数组,如果题目要求不能修改原来的数组,一个是可以使用哈希表,另一个是可以使用剑指Offer上给出的二分查找思路,但是相对比较麻烦。具体如下:
以长度为8的数组{2,3,5,4,3,2,6,7}为例,根据题目要求,这个长度为8的数组,所有元素都在1到7的范围内,中间的数字4将1—7分成两部分,分别为1—4和5—7,接下来统计1—4在数组中出现的次数,发现是5次,则说明这4个数字中一定有重复数字。接下来再把1—4分成1、2和3、4两部分,1和2一共出现了两次,3和4一共出现了3次,说明3和4中有一个重复,再分别统计即可得到是3重复了。这并不保证找出所有的重复数字,比如2就没有找到。
实际上,这种二分查找时间复杂度也达到了O(nlogn),不如用哈希表空间换时间来的直观。