选择问题

选择问题即:寻找N个元素中的第K个最大者。

选择问题的特殊情况是找最大者或最小者,这当然很简单了。还是一个特例找中位数。

《寻找N个元素中的前K个最大者》方法总结是在这里看到的 http://zhangliang008008.blog.163.com/blog/static/25136049200882423842325/,我觉得解法二和解法四用得广泛一些,编程实现了一下。

利用快速排序中的partition操作

经过partition后,pivot左边的序列sa都大于pivot右边的序列sb;

如果|sa|==K或者|sa|==K-1,则数组的前K个元素就是最大的前K个元素,算法终止;

如果|sa|<K-1,则从sb中寻找前K-|sa|-1大的元素;

如果|sa|>K,则从sa中寻找前K大的元素。

一次partition(arr,begin,end)操作的复杂度为end-begin,也就是O(N),最坏情况下一次partition操作只找到第1大的那个元素,则需要进行K次partition操作,总的复杂度为O(N*K)。平均情况下每次partition都把序列均分两半,需要log(2,K)次partition操作,总的复杂度为O(N*log(2,K))。

?
#include<iostream>
#include<cstdlib>
#include<ctime>
#include<vector>
#include<algorithm>
using  namespace  std;
 
/*分割,pivot左边的都比右边的大*/
template < typename  Comparable>
int  partition(vector<Comparable> &a, int  left, int  right){
     int  begin=left;
     int  end=right;
     Comparable pivot=a[left];
     while (left<right){
         while (left<right && a[right]<=pivot)
             right--;
         a[left]=a[right];
         while (left<right && a[left]>=pivot)
             left++;
         a[right]=a[left];
     }
     a[left]=pivot;
     
     return  left-begin;  //返回pivot左边的元素个数
}
 
template < typename  Comparable>
void  findKMax(vector<Comparable> &vec, int  left, int  right, int  k){
     if (k>right-left+1)
         return ;
     //由于partition时,总是固定地选取首元素作为轴,所以事先打乱一下顺序比较好,防止算法退化
     //random_shuffle(vec.begin()+left,vec.begin()+right);
     int  n=partition(vec,left,right);
     if (n==k || n==k-1)
         return ;
     if (n<k-1)
         findKMax(vec,left+n+1,right,k-n-1);
     else  if (n>k)
         findKMax(vec,left,left+n-1,k);
}
 
int  main(){
     int  total=5; //原先有5个元素
     int  k=3; //选取前3个最大的
     srand ( time (0));
     vector< int > a(total);
     for ( int  i=0;i<a.size();i++)
         a[i]= rand ()%100;
         
     for ( int  i=0;i<a.size();i++)
         cout<<a[i]<< "\t" ;
     cout<<endl;
     
     findKMax(a,0,a.size()-1,k);
     
     for ( int  i=0;i<a.size();i++)
         cout<<a[i]<< "\t" ;
     cout<<endl;
     
     return  0;
}

在选择pivot的时候传统的做法是选第1个元素作为pivot,一种优化的方法是随机选,更好的方法是三元取中法,更更好的方法是取五分化中项的中项,即把序列分为M组,每组5个元素,对每个组进行组内排序得到中项,然后对M个组按中项进行排序,取中间那个组的中项作为pivot

利用小根堆实现

顺序读取数组中的前K个元素,构建小根堆。小根堆的特点是根元素最小,并且一次调整(deleteMin)操作的时间复杂度为log(2,K)。

接下来从数组中取下一个元素,如果该元素不比堆顶元素大,则丢弃;否则用它替换堆顶元素,然后调整小根堆。

当把数组中的元素全部读出来后,小根堆中保留的就是前K大的元素。

初始建堆操作需要K*log(2,K)--这是最多的操作次数,从数组中读取后N-K个元素和堆顶元素一一比较,最坏的情况是每次都要替换堆顶元素,都要调整小根堆,复杂度为(N-K)*log(2,K)。总的复杂度为O(N*log(2,K))。

?
#include<iostream>
#include<cstdlib>
#include<ctime>
#include<vector>
using  namespace  std;
 
template < typename  Comparable>
void  percolate(vector<Comparable> &vec, int  index){
     int  i=index;
     int  j=2*i+1;
     while (j<vec.size()){
         if (j<vec.size()-1 && vec[j]>vec[j+1])
             j++;
         if (vec[i]<vec[j])
             break ;
         else {
             swap(vec[i],vec[j]);
             i=j;
             j=2*i+1;
         }
     }
}
 
template < typename  Comparable>
void  buildHeap(vector<Comparable> &vec){
     int  len=vec.size();
     for ( int  i=(len-1)/2;i>=0;i--)
         percolate(vec,i);
}
 
int  main(){
     srand ( time (0));
     vector< int > a(10);    //原先有10个元素
     for ( int  i=0;i<a.size();i++)
         a[i]= rand ()%100;
     
     vector< int > b(7); //找出a中最大的前7个元素
     for ( int  i=0;i<b.size();i++)
         b[i]=a[i];
     
     buildHeap(b);
     for ( int  i=b.size();i<a.size();i++){
         if (a[i]>b[0]){
             b[0]=a[i];
             percolate(b,0);
         }
     }
     
     vector< int >::iterator iter=a.begin();
     while (iter!=a.end()){
         cout<<*iter<< "\t" ;
         iter++;
     }
     cout<<endl;
     iter=b.begin();
     while (iter!=b.end()){
         cout<<*iter<< "\t" ;
         iter++;
     }
     cout<<endl;
     
     return  0;
}

用partition方法时需要知道总元素个数N,且内存中要能够容下这么多元素,而使用堆就完全没有这些限制了,堆的大小是K,内存中只要能容下这个堆就可以了。

原文来自:博客园(华夏35度)http://www.cnblogs.com/zhangchaoyang 作者:Orisun

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值