首先给出题目:在一个长度为n的数组里的所有数字都在0~n-1之间。数组中的数字是重复的,但具体不知道几个数字重复了,也不知道每个数字重复几次。请找出数组中的任意一个重复数字。
这道题看似简单,但实际上手仍然需要考虑很多种因素。一般的做法便是先将数组进行排序,然后比较前一个数字与后一个数字是否相等。如果相等就返回他。如果不相等就向后移动,知道数组结束。这种方法确实简单,而且很容易想明白。但排列一个长度为n的数据需要的时间为O(nlogn)的时间,很明显这种方法时间复杂度过高。
代码如下:
void Sort(int *arr,int len)
{
int tmp =0;
for( int i = 0;i<len;i++)
{
for(int j=0;j<len-1;j++)
{
if( arr[j] >arr[j+1] )
{
tmp = arr [j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
void SearchData(int *arr,int len)
{
Sort(arr,len);
int i = 0;
while(i < len)
{
if( arr[i] == arr[i+1] )
{
printf("%d ",arr[i]);
}
i++;
}
}
那我们换一种思路,首先数组的下标是从0开始,数据也是从0开始到n-1结束。那么我们可以先遍历整个数组,到扫描到下标为i的数字(m)时,将他与下标i做比较,如果不相等,就将他与第m个数字作比较如果相等,就找到了重复数字,如果不相等,则与第m个数字交换位置,直到找到重复数字。这种方法的好处就是活用了数组的下标与数组元素之间的关系,节约了时间,时间复杂度为O(n)。
现给出图解:
举个例子{3,2,4,0,3,1,5,7,8}。
3不等于0号下标,将3与3号下标数字0比较,发现不等相等,将0号下标与3号下标数字交换
0号下标与数字0相等,接下来重复以上操作,数字2与下标1不相等,将其与下标2数字4比较发现仍不相等,交换。
数字4与1号下标比较不相等,与4号下标数字3比较不相等,交换。
数字3与下标1不相等,与3号下标数字3比较发现,两者相等。此时便找到了重复元素3。
代码如下:
bool Searchdata(int *arr,int len,int *ddata)
{
if(arr==nullptr||len<=0)
{
return false;
}
for(int i=0;i<len;i++)
{
if(arr[i]<0||arr[i]>len-1)
{
return false;
}
}
for(int i=0;i<len;i++)
{
while(arr[i]!=i)
{
if(arr[i]==arr[arr[i]])
{
*ddata=arr[i];
return true;
}
int tmp=arr[i];
arr[i]=arr[tmp];
arr[tmp]=tmp;
}
}
return false;
}
接下来我们讨论一下影响因素,例如不能修改数组中的数字,从而找出重复数字。
对于这种情况的话,既然不能修改数组中的数字,那么我们可以利用一下辅助空间。首先创建一个大小为n+1的空间,将数组元素全部初始化为-1,作为标记。将原本的数组拷贝到辅助数组中,但这里注意的是将原本数组元素m拷贝到辅助数组下标m的位置。这样只需要一个判断语句(如果辅助数组m位置的元素与数组元素m不相等,就拷贝。若相等,就可以找到重复数字。
代码如下:
void Seachdata(int const *arr,int len,int *val)
{
if(arr==nullptr||len<=0)
{
return;
}
int *brr =new int[len+1]();
int i=0;
for(;i<len+1;i++)
{
brr[i]=-1;
}
i=0;
for(;i<len;i++)
{
if(brr[arr[i]]!=arr[i])
{
brr[arr[i]]=arr[i];
}
else
{
*val=arr[i];
break;
}
}
delete[]brr;
}
那要是不使用辅助空间呢?有没有好的办法可以解决呢?
当然有了,这时候我们可以思考一下既然是找到一个数字,而且是在数组中重复的。对于查找一个数字,最经典莫过于二分法查找(前提有序的)。我们这里可以引用一下二分法的思想,首先找出数组的中点m。他便将数组分成1 ~ m 和 m+1 ~ n-1两个区间。如果1 ~ m这个区间的数在整个数组中出现次数大于m那么重复数字便出现在1 ~ m这个区间,反之在m+1 ~n-1区间。然后将重复区域继续划分,直到找到重复元素。
例如:{3,2,4,0,3,1,5,7,8}。
数组的取值范围是 0 ~ 8, 中间元素3将其分为0 ~ 3与4 ~ 8 两个区间,然后我们统计0 ~ 3这4个数字在整个数组中出现的次数为 5,所以0-3这四个数字一定有一个数字重复。继续划分成0~ 1与2 ~ 3。2 ~ 3 在整个数组中出现了3次,所以 2 ~ 3 为重复区域。之后统计2与3 在数组中出现的次数,发现 3 出现两次所以 3便是重复数字。
这种方法,时间复杂度为O(nlogn),空间复杂度为O(1)。典型的以时间换时间。但这种方法不能找到所有的重复元素。
代码如下:
#include<iostream>
int Getcount(int const *arr,int len,int hhead,int ttail)
{
int count = 0;
int i=0;
for(;i<len;i++)
{
if(arr[i] >= hhead && arr[i] <= ttail)
{
++count;
}
}
return count;
}
int Searchdata(int const*arr, int len)
{
int head = 1;
int tail = len-1;
while(tail >= head)
{
int mid=(head+tail)/2;
int count=Getcount(arr,len,head,mid);
if(head == tail)
{
if(count>1)
{
return head;
}
else
{
break;
}
}
if( count>(mid-head+1) )
{
tail=mid;
}
else
{
head=mid+1;
}
}
return -1;
}