题目为:一个大小为n的数组,里面的数都属于范围[0, n-1],有不确定的重复元素,找到至少一个重复元素,要求O(1)空间和O(n)时间。
在博客:http://blog.youkuaiyun.com/morewindows/article/details/8204460 和 http://blog.youkuaiyun.com/morewindows/article/details/8212446 中给出了几种有效的解法。
解法二(类基数排序)的思路:
下面以2,4,1,5,7,6,1,9,0,2这十个数为例,展示下如何用基数排序来查找重复元素。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 2 | 4 | 1 | 5 | 7 | 6 | 1 | 9 | 0 | 2 |
(1)由于第0个元素a[0] 等于2不为0,故交换a[0]与a[a[0]]即交换a[0]与a[2]得:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 1 | 4 | 2 | 5 | 7 | 6 | 1 | 9 | 0 | 2 |
(2)由于第0个元素a[0] 等于1不为0,故交换a[0]与a[a[0]]即交换a[0]与a[1]得:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 4 | 1 | 2 | 5 | 7 | 6 | 1 | 9 | 0 | 2 |
(3)由于第0个元素a[0] 等于4不为0,故交换a[0]与a[a[0]]即交换a[0]与a[4]得:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 7 | 1 | 2 | 5 | 4 | 6 | 1 | 9 | 0 | 2 |
(4)由于第0个元素a[0] 等于7不为0,故交换a[0]与a[a[0]]即交换a[0]与a[7]得:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 9 | 1 | 2 | 5 | 4 | 6 | 1 | 7 | 0 | 2 |
(5)由于第0个元素a[0] 等于9不为0,故交换a[0]与a[a[0]]即交换a[0]与a[9]得:
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 2 | 1 | 2 | 5 | 4 | 6 | 1 | 7 | 0 | 9 |
(6)由于第0个元素a[0] 等于2不为0,故交换a[0]与a[a[0]]即交换a[0]与a[2],但a[2]也为2与a[0]相等,因此我们就找到了一个重复的元素——2。
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
数据 | 2 | 1 | 2 | 5 | 4 | 6 | 1 | 7 | 0 | 9 |
解法二、三(符号位标记法及改进版)的思路:
设int a[] = {1, 2, 1}
第一步:由于a[0]等于1大于0,因此先判断下a[a[0]]即a[1]是否小于0,如果小于,说明这是第二次访问下标为1的元素,表明我们已经找到了重复元素。不是则将a[a[0]]取负,a[1]=-a[1]=-2。
第二步:由于a[1]等于-2,因此先判断下a[-a[1]]取出a[2]是否小于0,如果小于,说明这是第二次访问下标为2的元素,表明我们已经找到了重复元素。不是则将a[-a[1]]取负,a[2]=-a[2]=-1。
第三步:由于a[2]等于-1,因此判断下a[-a[2]]即a[1]是否小于0,由于a[1]在第一步中被取反过了,因此证明这是第二次访问下标为1的元素,直接返回-a[2]即可。
但是,这种通过取负来判断元素是否重复访问的方法,当只有0是重复元素时是无法找出正确解的。
改进一下也很简单——不用取负,而用加n。这样通过判断元素是否大于等于n就能决定这个元素是未访问过的数据还是已访问过的数据。
全部代码:
#include <iostream>
#include <hash_set>
/*
运行环境:codeblocks
*/
using namespace __gnu_cxx;
using namespace std;
/*
解法一:采用 哈希表,时间复杂度符合要求,但是显然空间复杂度不符要求
*/
void func_1(int *a,int n)
{
hash_set<int> mySet;
bool flag = false;
for(int i=0; i<n; i++)
{
hash_set<int>::iterator iter = mySet.find(a[i]);
if(iter!=mySet.end()) //存在
{
flag = true;
cout<<"exist = "<<a[i]<<endl;
}
else
{
mySet.insert(a[i]);
}
}
if(!flag)
{
cout<<"not exits!"<<endl;
}
}
void Swap(int &a,int &b)
{
a = a^b;
b = a^b;
a = a^b;
}
/*
解法二:类基数排序法,该方法只能找出序列中一个重复的元素(算是一个小缺陷)
*/
void func_2(int *a,int n)
{
bool flag = false;
for(int i=0; i<n; i++)
{
while(i != a[i])
{
if(a[i] == a[a[i]])
{
flag = true;
cout<<"exist = "<<a[i]<<endl;
return;
}
else
{
Swap(a[i],a[a[i]]);
}
}
}
if(!flag) cout<<"not exist!"<<endl;
}
/*
符号位标记法:对于重复元素为 0 ,则不能满足要求
*/
void tempFunc(int *a,int n)
{
bool flag = false;
for(int i = 0; i < n; i++)
{
if(a[i] > 0) //判断条件
{
if(a[a[i]] < 0)
{
flag = true;
cout<<"exist = "<<a[i]<<endl;//已经被标上负值了,即有重复
}
else
{
a[a[i]]= -a[a[i]]; //记为负
}
}
else // 此时|a[i]|代表的值已经出现过一次了
{
if(a[-a[i]] < 0)
{
flag = true;
cout<<"exist = "<<-a[i]<<endl;//有重复找到
}
else
{
a[-a[i]] = -a[-a[i] ]; //记为负
}
}
}
if(!flag)//数组中没有重复的数
{
cout<<"not exist!"<<endl;
}
}
/*
解法三:符号位标记法的改进版:满足要求。
*/
void func_3(int *a,int n)
{
int index;
bool flag = false;
for(int i=0; i<n; i++)
{
if(a[i] >= n) index = a[i] - n;
else index = a[i];
if(a[index] >= n) //这个位置上的值大于n说明已经是第二次访问这个位置了
{
flag = true;
cout<<"exist = "<<index<<endl;
}
else a[index] += n;
}
if(!flag)//数组中没有重复的数
{
cout<<"not exist!"<<endl;
}
}
int main()
{
const int MAXN = 10;
int a[MAXN] = {0, 4, 1, 5, 7, 6, 1, 9, 0, 2};
func_3(a,MAXN);
return 0;
}