题目1:在一个长度为n的数组中所有数字的范围都在0~n-1的范围内,找出任意一个重复的数字,不存在的话返回-1。
解题思路1
先将数组排序,排序后重复的数字就会在相邻的位置,那么只要扫描一遍数组,对比相邻的数字就可以找出重复的数字。排序时间复杂度是O(nlogn),扫描数组复杂度是O(n),因此总的时间复杂度是O(nlogn),空间复杂度O(1),需要改变原数组。
int duplicate(vector<int> numbers,int length)
{
sort(numbers.begin(),numbers.end());
for(int i=1;i<length;i++)
if(numbers[i]==numbers[i-1])
return numbers[i];
return -1;
}
解题思路2
将数组排序的时间复杂度比较高,要更快地完成问题,我们需要想其他的方法。由于每个数字的值都在数组的下标的范围内,而且数组是随机存取的,赋值和访问的时间复杂度都是O(1),因此我们可以将值为k的数组放置在下标k的位置,通过扫描数组,每次都将一个数字放置到正确的下标位置,就能在最多n次操作后找到重复的数字(或者发现没有重复数字)。算法的做法是从下标0出发,(1)如果值与下标一致,则考量下一个下标;(2)如果值与下标不一致,如下标为index,值为k,如果k位置上的值已经是k,则k为重复值,否则交换index和k两个位置上的数,然后重复以上步骤。算法总的时间复杂度是O(n),空间复杂度O(1),需要改变原数组。
void swap(int& a,int & b)
{
int tmp = a;
a = b;
b = tmp;
}
int duplicate(vector<int> numbers,int length)
{
for(int i=0;i<length;i++){
while(numbers[i]!=i){
if (numbers[numbers[i]]==numbers[i])
return numbers[i];
else
swap(numbers[i],numbers[numbers[i]]);
}
}
return -1;
}
解题思路3
如果不允许改变原数组,又需要用最快的速度完成任务,那么就可以考虑使用空间换时间。使用打表法,额外建立一个O(n)的数组,由于数组赋值和读取是O(1)时间的,因此对于每个数都可以O(1)时间内检查是否前面已经出现过,并在辅助数组中对应下标的位置做标志,总的时间复杂度是O(n),空间复杂度是O(n),不需要改变原数组。
int duplicate(vector<int> numbers,int length)
{
vector<bool> flag(length,false);
for(int i=0;i<length;i++){
if(flag[numbers[i]])
return numbers[i];
flag[numbers[i]] = true;
}
return -1;
}
题目2:一个长度为n+1的数组中所有的数字范围都在1~n,找出其中任意一个重复的数字。
第二题是第一题加强了约束的版本,第一题可能存在没有重复数字的情况,而第二题根据鸽笼定理至少会有一个数字重复,因此除了上述的三种方法可以使用外,在不改变原数组,不使用额外空间的情况下还可以用二分的方法做。
解题思路4 二分法
将数字的范围二分成两个范围,总有一边数字数量会大于数字范围,根据鸽笼定理那边一定有重复数字。
二分的次数为O(logn)次,每次需要统计数字的时间复杂度是O(n),因此总的时间复杂度是O(nlogn),空间复杂度O(1),不需要改变原数组。
int duplicate(vector<int> numbers,int length)
{
int left =1, right = length-1;
while(left<right){
int mid = (left+right)/2;
int count = 0;
for(int i=0;i<length;i++)
if (numbers[i]>=left && numbers[i]<=mid)
count ++;
if(count>mid-left+1)
right = mid;
else
left = mid + 1;
}
return left;
}