检验数组中是否存在重复元素是一个非常常见的问题,存在很多的解决方法,下面将一一介绍各种方法,并在不同的题设要求下给出不同的解决方法,其中包含了很多算法设计思路,相信对学习算法的您会有一定的帮助和启发。
假设一个数组中含有N个元素,其中这N个元素分布在[1,N]的整数,请你判断数组中是否存在重复的元素。
在没有学习hash表之前,我们最容易想到的一个办法就是数组排序,使用堆排序对数组进行排序,然后扫描数组,若存在相邻的两个数组元素相等,则返回此元素,上述算法的时间复杂度为O(nlogn),还算是比较快的算法。
算法的实现如下所示:
int find_a_duplicate_sort(int* a, const int n)
{
heap_sort(a,n);
for(int i=0; i
但是学习了hash表之后发现上述算法实在是太慢了,我们可以牺牲一定的存储空间来保证算法的时间复杂度下降。如何利用hash表来实现上述功能呢?
首先用数组元素作为hash表的键值对为(数组元素的值,此元素的个数),设计这样的hash表就可以在线性时间内完成上述功能,具体实现如下所示。
int find_a_duplicate_hash(const* int a, const int n)
{
map hash;
for(int i=0; i::iterator it;
it = hash.find(a[i]);
if(it == hash.end())
{
hash[a[i]] = 1;
}
else
{
return a[i];
}
}
return -1;
}
通过上述两个算法的探讨知道,可以在线性时间内解决上述问题。那么问题来?(挖掘机技术哪家强?)我们能否在线性时间解决上述问题的同时保证空间复杂度为常数,即空间复杂度为O(1)呢?再次阅读题目发现,我们在上述解决方法都没有用到一个条件,那就是这些数分布在[0,N]之间,且数组只有N个元素。为此对于这个特殊的数组我们可以利用这个样的排序方法来排序,即第一个元素为1,第二个元素为2,第3个元素为3,……,第n-1个元素为n-1,第n个元素为n,那么如果存在重复的数字的话,在排序的过程中将会出现冲突。现在来看看这个特殊性给我们带来了什么好处。
假设数组a[5]={1,3,2,4,1},先来看看如何排序,首先a[0] = 1,满足条件,a[1]=3,不满足条件(a[1] = 2),此时我们将3先放到它应该在的位置(a[2] = 3),为此交换a[1]与a[2]值,此时变为a[1]=2,a[2]=3,再检查a[1]是否满足条件,现在恰好a[1]满足a[1]=2的条件(巧合……),然后依次检查a[2],a[3],a[4],在a[4]时不满足条件a[4] = 5,此时我们先检查一下a[4]的值是否已经存在,因为a[4]的值等于1,我们只需检查a[1-1],即a[0]的值是否等于1即可判断这个数是否出现过,显然,出现过,从而返回a[4]的值,它即为重复的数字。
算法实现:
int find_a_duplicate(int* a, const int n)
{
for(int i=0; i
依据数组的特殊性,先对数组从0开始排序(实际上是记录),不停的交换a[0]与a[a[0]-1]的值,直到a[0]=1为止,然后对i=1,2,3,4,……,n-1重复上述步骤,如果a[i] != i+1,说明这个下标没有排序过,现在检查这个元素的值是否存在,如果这个值-1对应的下标排序过,那么这个下标对应的值就和当前未排序的这个下标对应的值相等,从而说明这个值即为重复的元素,因此就找到了数组中的元素。
上述算法虽说有两个循环,但是内循环在循环的过程中不停的将数组中的元素调整为不满足内循环条件的元素,从而减少了内循环的次数,因此上述算法平均时间复杂度接近于线性,即O(n),额外的空间复杂度为O(1)。
但是上述算法存在一个严重的问题就是在查找数组重复元素的过程中,改变了数组中元素的次序。那么是否存在不改变数组顺序且性能与上述算法相同的算法呢?当然存在,这个方法的受到了寻找链表中是否存在环的启发。因为我们的数组的元素的值与下标都来自与一个共同的数组,即类似于链表中节点与指针。从而想到利用查找链表中的环的方法来查找数组中的重复元素。
使用两个index,分别为index_slow和index_fast,其中index_slow每次走一步,index_fast每次走两步,如果链表中存在环,则上述两个index必然会相遇,即相等。此时说明数组中存在重复的数字。那怎么找到这个重复的数字呢?如下图所示
假设在交点处相遇,设圆环的长度为L,则index_slow走了X1+L-X2,index_fast走了L+L+X1-X2,根据假设可知2(X1+L-X2)=X1+L-X2+L
化简得X1=X2,从而可知只需再利用一个index从起点走,index_slow从交点走,则两者必然在圆环的起点相遇,此时index对应的数组元素即为重复的数组元素。
算法实现:
int find_a_duplicate_update(const int* a, const int n)
{
/************************************************************************/
/* 当数组含有n个元素时,元素的最大取值为n-2,最小值为0 */
/*由于这个规定,上述数组中必然会存在重复的元素 */
/************************************************************************/
if(n==1)
return -1;
int index_slow = n-1;
int index_fast = n-1;
index_fast = a[index_fast];
index_fast = a[index_fast];
while (index_slow != index_fast)
{
index_slow = a[index_slow];
index_fast = a[index_fast];
index_fast = a[index_fast];
}
int index_res = n-1;
/************************************************************************/
/* index_slow 先走两步,保证index_slow到交汇点的距离与index_res的相同 */
/************************************************************************/
index_slow = a[index_slow];
index_slow = a[index_slow];
while (index_slow != index_res)
{
index_res = a[index_res];
index_slow = a[index_slow];
}
int res = index_res;
return res;
}
算法分析,上述算法最多需要扫描数组两遍即可得到数组中的重复元素,因此算法的时间复杂度为O(N),现在额外的空间只有3个变量,即额外的空间复杂度为O(1),且在查找重复元素的过程中没有改变数组元素的次序。
测试代码:
#include
using namespace std;
int find_a_duplicate(int* a, const int n);
int find_a_duplicate_update(const int* a, const int n);
void main()
{
int a[] = {1,0,1,2,3,4,5};
//cout<
上述算法仅供参考学习,如有什么不对之处还请多多指出批评,欢迎拍砖