选择问题

选择问题即:寻找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,内存中只要能容下这个堆就可以了。 

posted @ 2011-11-05 09:48  张朝阳  阅读(3575)  评论(0编辑  收藏  举报