下面可以谈论下如何求频繁元素的一个问题。
一、问题定义
如果一个数据流,其中m为数据流的大小,。我们可以定义每个元素出现的次数为,其中为第i个元素出现的次数。容易得出:。
如果给定参数k,我们想求出所有出现次数超过m/k的元素。也就是输出集合:。下面我们先从一个简单特例入手,对这个问题进行分析和解决。
二.简单情况
有个经典的过半元素查找问题,在编程之美里面也有分析(寻找发帖水王)。就是说给定一个数组,找出出现次数超过一半的元素。对于这个问题一个比较好的做法就是不断删除两个不同的元素,最终剩下的元素就是所要求的元素。使用一个变量ans作为候选。该方法时间复杂度为O(n),空间复杂度为O(1)。具体实现如下:
int majority_element(vector<int>nums) { int len=nums.size,count=0,ans=0; for(int i=0;i<len;i++){ if(count==0){ ans=nums[i]; count++; }else{ if(ans==nums[i]){ count++; }else{ count--; } } } return ans; }
三、一般情况
编程之美里面在解决了这个问题之后,提出了一个扩展问题,就是说如何找出发帖总数目的1/4的ID。一般化的情况就是我们在第一节中定义的问题。那一般化的问题如何解决呢?
该问题的解决方法就是对第二节中方法的改进。第二节中k=2,使用一个变量作为候选。这里,我们使用一个大小为k-1的变量作为候选,因为至多有k-1个元素的出现次数超过m/k,否则不符合实际情况。具体实现我们可以使用一个map,map里存放元素及其出现的次数。访问到第i个元素的时候,如果这个元素在map中,则将对应出现次数加1。否则看map的大小。如果map的大小少于k-1,我们将其添加到map中,设其出现次数为1。否则我们将map里面所有的元素次数都减一,如果某元素对应的次数为0,则从map中删去该元素。具体实现代码如下。
void frequent_element(vector<int>nums,int k) { map<int,int>num_cnt_map; for(int i=0;i<nums.size();i++){ if(num_cnt_map.find(nums[i])!=num_cnt_map.end()){ num_cnt_map[nums[i]]=num_cnt_map[nums[i]]+1; }else if(num_cnt_map.size()<k-1){ num_cnt_map[nums[i]]=1; }else{ for(map<int,int>::iterator it=num_cnt_map.begin();it!=num_cnt_map.end();it++){ num_cnt_map[it->first]=num_cnt_map[it->first]-1; if(num_cnt_map[it->first]==0){ num_cnt_map.erase(it); } } } } }
下面我们对算法进行分析。在map中的元素次数都减一这个步骤(第9行到第14行的循环),一共减去了k(map中总共k个元素,每个元素对应次数减1)。所有元素出现的总次数为m,所以该步骤至多执行m/k次。显然任意出现次数超过m/k的元素一定最后仍留在map中。但是上述算法有个缺点就是:如果出现次数超过m/k的元素非常少,那么最终map中的某些元素不一定大于m/k。最终map中元素可能也没有k-1个。这时候我们需要对原数据流再做一次扫描,从而确定候选元素的出现次数,从而得出精确解。
四、进一步分析
我们可以通过第三节中的算法来对频繁元素出现的次数进行估计。对于任意频繁元素j,其出现次数为,最终估计值就是算法执行结束后num_cnt_map[j]。则两者有如下关系:
如果我们知道算法处理结束时map中元素对应次数的和为,所以map中元素次数都减一这个步骤总共减去了,所以最多执行了次。则上面的不等式中的下界可以进一步提高: