您的位置:首页 > 其它

查找出现次数大于n/k的重复元素 ---非多重集算法

2011-12-04 10:07 288 查看
本文是对一篇英文论文的总结:Finding Repeated Elements。想看原文,请Google之。

这个问题的简单形式是“查找出现次数大于n/2的重复元素”。我们先从简单问题开始,然后再做扩展。

1.查找出现次数大于n/2的重复元素

  《编程之美》中有同样的一道题《寻找发帖水王》,具体思路是每次删除两个不同的元素,最后剩下的就是要求的元素。这个结论的证明如下:

  已知:n,m是正整数,n表示数组的长度,m是出现次数大于n/2的元素的个数,即m>n/2。

  需要求证的结论包括两个:

 (1)我们用v表示出现次数大于n/2的元素。当删除两个不同元素,且其中有一个元素是v时,则m减小1,同时n要减小2。

  求证:m-1>(n-2)/2

  证明:m-1>n/2-1=(n-2)/2

 (2)当删除两个不同元素,且其中有一个元素不是v时,则只需要n减小2。

  求证:m>(n-2)/2 。这个结论是显然的。

代码如下:

int find(int array[], int n)
{
int candidate;
int count=0;
for(int i=0;i<n;++i)
{
if(count==0)
{
candidate=array[i];count=1;
}
else
{
if(candidate==array[i])
++count;
else
--count;
}
}
return candidate;
}


《编程之美》的后面习题是“查找出现次数大于n/4的元素”,思路是每次删除不同的4个元素,最后剩下的3个就是候选元素,但是还要验证这3个元素是否满足条件。不再详细解释。其实《编程之美》里讲的方法就是本文后提到的“多重集”算法。

首先讲论文里的那种非多重集算法。用到的变量如下:

b[]:元素所在的数组

i:数组索引

v:保存要查找的值,就是说,当遍历过n个元素之后,v的出现次数大于n/2

c:v出现次数的上限的两倍,即v的出现次数小于等于c/2。

初始化,i=0,v=null ,c=0。

遍历b[]数组,每当遍历到b[i]时,有两种情况会发生:

(1)b[i]=v

  因为b[i]=v,所以v的出现次数又增加1,为了维护c的含义,即让c/2也增加1,那么c要增加2。

(2)c=i

  这一步,我们要证明b[0...i-1]中任何元素的出现次数都不超过i/2,那么必须更新v。

  我们先推出前一次遍历时c和i的关系: c每次增加2,i每次增加1,为了使本次遍历时让c=i,那么在前一次遍历中,i肯定是增加了1,而c没有增加。所以在前一次遍历之前c和i的关系是c=i+1,而前一次遍历结束时c才与i 相等。

  再考察前一次遍历时的情形:我们令m表示v的出现次数,那么在前一次遍历之前的情况是 m>i/2 且 m<=
c/2=(i+1)/2,则有式子:i/2<m<=(i+1)/2,注意,此时i是奇数(根据c=i+1,c是偶数),那么m只能等于(i+1)/2,即m=c/2。

  而本次遍历面临的情况是c=i,在数组b[0...i-1] 中,v的出现次数是c/2,而除去v之外的其他元素的出现次数是i-m=i-c/2=i-i/2=i/2,这说明b[0...i-1]里任意一个元素的出现次数都没有超过i/2,那么我们必须更新v。最有可能的元素就是b[i],其实有可能最终b[i]也不是。

代码:

const int N=9;
int b
={2,3,3,2,2,2,1,1,2};
int v=-1, c=0, i=0;
while(i!=N)
{
if(v==b[i])
{
c+=2;
}
else if(c==i)
{
v=b[i];
c+=2;
}
++i;
}
cout<<v<<endl;

如果不存在满足条件的v,那么v的取值是最后一个数。

2.查找出现次数大于n/k的元素

给定2<=k<=n和数组b[0...n-1],我们要查找出现次数大于n/k的元素。

在问题1.中,实际上是把数组分成了两个集合,一个集合只包含一个元素v(或空),另一个集合包含出现次数小于等于n/2的元素。类似地,我们将其扩展,仍然是把数组分成两个集合,一个集合包含出现次数可能大于n/k的元素,一个集合包含出现次数小于等于n/k的元素。

相关变量声明如下:

v:出现次数可能大于n/k的元素

c:v的出现次数不超过c/2

集合t:包含的是形如(v,c)的值对。

s:对于出现次数小于等于n/k的元素,我们设其出现次数的上限为s/k。

算法包含两个阶段:首先,选出集合t;然后,验证t中的元素是否满足条件。我们在这里只描述第一阶段。第二阶段的复杂度是O(n*log(t))。

i=0,s=0,t={};
for(;i<n;++i)
{
在t中找出v=b[i]的元素(v_j,c_j),j是其下标; 如果不存在这样的元素,则令j=0;
if(j==0 && s+k<=i+1)
{
s=s+k;
}
else if(j==0 && s+k>i+1)
{
把(b[i],s+k)加入集合t中;
}
else // j!=0
{
c_j=c_j+k;
}
i=i+1;
删除t中所有满足c=i的元素。若有元素被删除,则设s=i;
}

下面分别解释一下3个if语句:

(1) j=0且s+k<=i+1

如果没有找到v=b[i]的元素,那么就要判断当前元素b[i]是否应该放入集合t中。如果b[i]不应该放入集合t中,说明本次迭代之后b[i]出现的次数上限(s+k)/k<=(i+1)k,即有条件:s+k<=i+1。

(2) j=0且s+k>i+1

如果b[i]应该放入集合t中,说明本次迭代之后b[i]出现的次数上限(s+k)/k>(i+1)k,即有条件:s+k>i+1。

(3) j!=0
如果找到v=b[i]的元素,那么就要更新c_j的值。因为v_j的出现次数增加1,所以c_j/k也增加1,即令c_j = c_j + k。

当处理完b[i]之后,可能集合t中的某些元素的c值已经过期了。过期的条件是什么呢?因为有条件:c/k >= (v的出现次数) > i/k,即 c/k>i/k,化简为c>i。i是递增的,而c有可能没有增加,所以当c=i 时,c就过期了。这时就要把这样的元素从t中删除。为什么还要更新s的值呢?我还没想明白。

算法第一阶段的复杂度也是O(n*log(t))。而t最多有多少个元素呢?论文中说|t|最大值是k*log(k)。所以算法总体复杂度是O(n*k*log(k))。


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐