面试题3——找出数组中重复的数字(反证法)
在一个长度为n的数组里所有整数都在m~m+n-1范围内,(注意原题是长度为n的所有数字都在0~n-1的范围内,但这里我强调了范围的偏移m,随着解释会慢慢清楚的)。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组[3,5,-1,-2,0,3,5],那么对应的输出是数字3或5.
解析:笨方法是来一个i、i+1的遍历,使数组中所有整数都两两比较,能找出所有重复数字,这时间复杂度是O(n2),显然是很大的。或者来一个排序,然后走一趟遍历,也能找出所有重复数字,还能比较直接地统计出每个重复数字的重复次数,时间复杂度是O(nlogn)+O(n)。
然,还有一种神奇的算法能实现时间复杂度O(n)并能找出所有重复数字:注意到n维数组所有元素的区间都落在m~m+n-1范围内,注意从m到m+n-1之间满打满算也只有n个整数,则假设数组没有重复的元素(反证思想),则排序后的数组就是从小到大的[m,m+1,m+2,...,m+n-2,m+n-1],是紧密相连的,每个元素在有序数组中都有自己独有的位置,并且有规律:整数x一定在有序数组的x-m下标处,因为是密布的整数。
所以,如果数组中有重复整数y,那在第二个y要填到有序数组中时就会发现其下标y-m处已经被第一个y给占了,此时就是检测到重复整数y了。
算法:
#include <iostream>
int RepetNum(int arr[],size_t len,int *&arrRepet){
/*
首先统计数组的区间[m,n],若是n-m+1==len,也就是数组最小值m到最大值n之间如果密布整数的话
则一共会有n-m+1个数,正好是待考察数组的元素总数,满足这个巧合才能用"假设数组密布则元素k在
有序数组中下标为k-m"来判断考察数组是否密布(若不密布则一定有重复元素,重复元素必定计算出
同一下标)
*/
int nmin(arr[0]),nmax(arr[0]);
for(size_t i=1;i<len;i++){
nmin=arr[i]<nmin?arr[i]:nmin;
nmax=arr[i]>nmax?arr[i]:nmax;
}
if( !((nmax-nmin+1)==len) ) {
arrRepet=nullptr;
return -1;//不适用于本法
}
else{
//可假设区间满布来检查重复元素
int arr2[len]; //有序数组
for(size_t i=0;i<len;i++) arr2[i]=nmin-1; //有序数组初始化为[nmin,nmax]之外的值,防止误判
arrRepet = new int [len/2]; //最多有len/2个重复的元素
size_t nCount(0); //重复元素组数
for(size_t i=0;i<len;i++){
if(arr[i]==arr2[arr[i]-nmin]){
//arr[i]被期望在有序数组arr2的下标arr[i]-nmin处,如果有序数组
//arr2[arr[i]-nmin]已经有值arr[i]了,那说明此时的arr[i]是重复的
arrRepet[nCount++]=arr[i];
}else{ //arr[i]是同值第一个出现的,放到有序数组arr2中
arr2[arr[i]-nmin]=arr[i];
}
}
return nCount;
}
}
int main(int argc,char *argv[]){
int arr[]={9,7,8,7,8,4};
int nRepet(0);
int *arrRepet(nullptr);
nRepet=RepetNum(arr,6,arrRepet);
}