参考:http://blog.csdn.net/xiaoding133/article/details/8037086
问题:查找大量无序元素中最大的K个数,姑且假定它们各不相等。
解法一:该解法是大部分能想到的,也是第一想到的方法。假设数据量不大,可以先用快速排序或堆排序,他们的平均时间复杂度为O(N*logN),然后取出前K个,时间复杂度为O(K),总的时间复杂度为O(N*logN)+O(K).
当K=1时,上面的算法的时间复杂度也是O(N*logN),上面的算法是把整个数组都进行了排序,而原题目只要求最大的K个数,并不需要前K个数有限,也不需要后N-K个数有序。可以通过部分排序算法如选择排序和交换排序,把N个数中的前K个数排序出来,复杂度为O(N*K),选择哪一个,取决于K的大小,在K(K<logN)较小的情况下,选择部分排序。
事实上我们可以避免对前K个数排序来获得更好的性能。
解法二:避免对前K个数进行排序来获取更好的性能(利用快速排序的原理)。
假设N个数存储在数组S中,从数组中随机找一个元素X,将数组分成两部分Sa和Sb.Sa中的元素大于等于X,Sb中的元素小于X。出现如下两种情况:
(1)若Sa组的个数大于或等于K,则继续在sa分组中找取最大的K个数字 。
(2)若Sa组中的数字小于K ,其个数为T,则继续在sb中找取 K-T个数字 。
一直这样递归下去,不断把问题分解成小问题,平均时间复杂度为O(N*logK)。
//下面的伪代码是解决寻找最大的K个数
/*
思路:将原始Array一分为二,左边为Sa,右边为Sb,显然Sa中每个元素都要大于Sb中每个元素
if(Sa.length < K) 返回Sa,并到Sb中寻找K-Sa.length个最大的
else 继续在Sa中寻找K个最大的
*/
Array findBigK(Array , int K)
{
if(Array.length<=0)//边界条件
return [];
if(Array.length <= K)
return Array;
[Sa , Sb] = partition(Array);//得到Array的一个随机划分
if(Sa.length < K)
return Sa.append(findBigK(Sb,K-Sa.length));
else
return findBigK(Sa,K);
}
[Sa , Sb] partition(Array)
{
Sa=[];
Sb=[];
swap(Array[0] , Array[Random()%Array.length]);//随机主元
int pivot = Array[0];
for(int index=1 ; index<Array.length ; index++){
if(Array[index] >= pivot)
Sa.append(Array[index]);
else
Sb.append(Array[index]);
}
//将主元加入长度较小的组,让两数组分布更均匀,提高效率。还可避免分组失败
Sa.length>Sb.length?Sb.append(pivot):Sa.append(pivot);
return[Sa , Sb];
}
实现代码如下:
void swap2Num(int *a , int *b)
{
int temp = *a;
*a = *b;
*b=temp;
}
int Partition(int Array[] , int low , int high)
{
srand(time(0));int pivot = Array[low + rand()%(high-low+1)];
while(low < high){
while(low<high && Array[high]<pivot)
high--;
if(low<high)
swap2Num(&Array[high],&Array[low]);
while (low<high && Array[low]>pivot)
low++;
if(low<high)
swap2Num(&Array[high],&Array[low]);
}
return high;
}
int findBigK(int Array[] ,int low , int high , int K)
{
int kIndex = -1;
int pivot_index = Partition(Array , low , high);
int SaLen = pivot_index-low+1;//本次迭代Sa长度
if(low < high){
if(SaLen == K)
return pivot_index;
else if(SaLen > K)//Sa.length > K
kIndex = findBigK(Array , low , pivot_index-1 , K);
else //Sa.length < K
kIndex = findBigK(Array , pivot_index+1 , high , K-SaLen);
}
return kIndex;
}
int main()
{
int Array[7]={1,6,5,8,4,2,9};
int index = findBigK(Array , 0 , 6 , 4);
cout<<"划分后数组为:"<<endl;
for(int i=0 ; i<6 ; i++)
cout<<Array[i]<<" ";
cout<<endl;
cout<<"下标为:"<<findBigK(Array , 0 , 6 , 4)<<endl;
for(int i=0 ; i<=index ; i++)
cout<<Array[i]<<" ";
cout<<endl;
return 0;
}
解法三:这也是寻找N个数中的第K大的数算法。利用二分的方法求取TOP k问题。 首先扫描一次查找max 和 min,然后计算出mid = (max + min) / 2,然后扫描一次查看是大于mid数的个数,不断的迭代缩小min与max,最终min与max区间内只有一个数,就是所求的第K大的数。该算法的实质是寻找最大的K个数中最小的一个。算法代码如下:
/*
算法分析:
对于这个未排序数组遍历一遍整个集合,统计在该集合中大于等于某个数的整数有多少个
,不需要作随机访问,而只需要经过统计确定新的min和max后更新数组存放文件,扫描一次
把在新区间[min,max]内的数存放在原文件中,这样下一次操作时候,不再须遍历全部元素。
这样做的好处在于每次更新解区间后,元素数目会减少。
由求解过程可以看出扫描太多次了,因此对于O(N*logN)的算法常数项很大,实际应用中
效果有时并不好
*/
int findOverNum(int Array[] , int length , int value)
{
int count = 0;
for(int i=0 ; i<length ; i++){
if(Array[i] > value)
count++;
}
return count;
}
int findBigK(int Array[] , int length , int K)//返回第K大的数
{
const double deleta = 0.5;//deleta比数组内最小的元素差值还要小
//扫描一遍得到最大值和最小值
int vMin = Array[0];
int vMax = Array[0];
for(int i=1 ; i<length ; i++){
if(Array[i] >= vMax)
vMax = Array[i];
if(Array[i] <= vMin)
vMin = Array[i];
}
int vMid = 0;
/*
下解是书上的解法,当数组b作为输入的时候,vMid将会重复计算为3,
此时vMin=3,vMax=4,而大于3的个数为5所以会重复置vMin=3,陷入死循环
症结在于此时其实区间内只有一个数了,但是deleta设定为0.5,而vMax与vMin之间的差值恒为1
*/
// while(vMax-vMin > deleta){//解区间内大于一个数
// vMid = vMin + (vMax-vMin)/2;//防止上溢
// int overNum = findOverNum(Array,length,vMid);//大于vMid的数的个数
// if(overNum >= K)
// vMin = vMid;
// else
// vMax = vMid;
// }
//-----------------------------------------------------------------
/*
下解是本人的解法,可以加入判断条件避免这样的情况发生
*/
while(vMax-vMin > deleta && vMin<vMax-1){//解区间内大于一个数,且vMin与vMax不相邻
vMid = vMin + (vMax-vMin)/2;//防止上溢
int overNum = findOverNum(Array,length,vMid);//大于vMid的数的个数
if(overNum >= K)
vMin = vMid;
else
vMax = vMid;
}
//-----------------------------------------------------------------
//解区间只有一个数
int result = 0;
for(int i=0 ; i<length ; i++){
if(Array[i]>=vMin && Array[i]<=vMax){
result=Array[i];
break;
}
}
return result;
}
int main()
{
int a[8] = {54, 2 ,5 ,11 ,554 ,65 ,33 ,56};
int b[7] = {1,2,4,6,7,6,8};
//int x = findBigK(a,8,2) ;
int y = findBigK(b,7,5);
cout<<" "<<y<<endl ;
return 0 ;
}
可以看到deleta的取值关系到算法时间复杂度,每次算法迭代都与deleta进行比较,该算法常数项较大,实际应用中效果并不好。
解法四:我们已经得到了三种解法,这三种解法通常都需要对数据访问多次。如果N非常大呢?无法装入内存,这种数据访问非常耗时,能否考虑尽量减少遍历次数呢?
可以使用容量为K的最小堆来存储最大的K个数,最小堆的堆顶元素就是最大K个数中最小的一个,当考虑一个新数X,如果X比对顶元素大,则用X替代堆顶元素并更新堆(维持堆的性质),更新过程花费O(logK),算法只需要扫描所有数据一次,时间复杂度O(N*logK),空间上只需要维护容量为K的堆。
下面是更新堆的伪代码:
//更新堆的伪代码
int updateHeap(int h[] , int x)
{
if(x > h[0]){//扫描元素大于堆顶元素,将x插入堆顶并维护
h[0] = x;
p=0;//堆顶下标
while(p < K){//当下标不超出堆容量时遍历左子树和右子树寻找插入点
q = 2*p+1;//左孩子
if(q > K)
break;
if(q+1<K && h[q+1]<h[q])//右孩子小于左孩子
q=q+1;//令最小的是右孩子
if(h[q] < h[p]){
temp = h[p];
h[p] = h[q];
h[q] = temp;
p = q;//向下遍历最小堆
}
else
break;
}
}
}
下面是该算法的实现代码:
//递归的维护这个最小堆,i是维护的顶点,由于用数组作为数据结构,下标从0开始,hsize为堆元素个数
void minHeapify(int H[] , int hsize , int i)
{
int leftChild = 2*i+1;
int rightChild = 2*i+2;
int minIndex = i;//设定起始最小值下标
if(i < hsize){
if(leftChild < hsize){
if(H[leftChild] < H[minIndex])
minIndex = leftChild;
}
if(rightChild < hsize){
if(H[rightChild] < H[minIndex])
minIndex = rightChild;
}
if(i != minIndex){
int temp = H[i];
H[i] = H[minIndex];
H[minIndex] = temp;
minHeapify(H , hsize , minIndex);
}
}
}
//自底向上建立最小堆
void createHeap(int H[] , int hsize)
{
for(int i=hsize-1 ; i>=0 ; i--){
minHeapify(H , hsize , i);
}
}
//Array为原数组,找到最大的K个元素存放在最大堆KMax中
void findBigK(int Array[] , int ArrayLen , int K , int KMax[])
{
if(ArrayLen > K){
for(int i=0 ; i<K ; i++)
KMax[i] = Array[i];//初始化数组KMax
createHeap(KMax , K);//以原始数组建立大小为K的最小堆
for(int j=K ; j<ArrayLen ; j++){
if(Array[j] > KMax[0]){
KMax[0] = Array[j];
minHeapify(KMax , K , 0);
}
}
}
else{
printf("K < ArrayLen !\n");
return;
}
}
int main()
{
int a[] = {10,23,8,2,52,35,7,1,12};
int aLen = sizeof(a)/sizeof(int);
const int K=4;
int *KMax = new int[K];
findBigK(a , sizeof(a)/sizeof(int) , K , KMax);
for(int i=0 ; i<K ; i++)
printf("%3d" , KMax[i]);
printf("\n");
delete KMax;
return 0;
}
解法五:通过改变计数排序算法可以得到下面的算法:
如果N个数都是正数,取值范围不太大,可以考虑用空间换时间。申请一个包括N中最大值的MAXN大小的数组count[MAXN],count[i]表示整数i在所有整数中的个数。这样只要扫描一遍数组,就可以得到第K大的元素。
/*
使用一个数组count[MaxN]记录每个数出现的次数,
N个数的最大数为MaxN。最好被查找数据尽量紧凑且全为正整数才有效。
返回第K大的值,只要再通过一次扫描就能找到所有K个数
*/
int findBigK(int Array[] , int ArrayLen ,int K)
{
//扫描一次找最大MaxN
int MaxN = -1;
for(int i=0 ; i<ArrayLen; i++){
if(Array[i] > MaxN)
MaxN = Array[i];
}
/*
利用计数数组,count[i]下标[0,MaxN-1],每个下标i
记录数值i+1出现的次数。
*/
int *count = new int[MaxN];
memset(count,0,sizeof(int)*MaxN);
for(int i=0 ; i<ArrayLen ; i++){
count[Array[i]-1] ++;
}
//一次扫描自底向上找K个数,找到的值放在kValue中,注意到count数组下标i与数组数字i+1的关系
int sum = 0;
int kValue;
for(kValue=MaxN-1 ; kValue>=0 ; kValue--){
sum += count[kValue];
if(sum >= K)
break;
}
delete count;
return kValue+1;
}
int main()
{
int a[] = {10,23,8,2,52,35,7,1,12};
int aLen = sizeof(a)/sizeof(int);
const int K=3;
cout<<findBigK(a , sizeof(a)/sizeof(int) , K)<<endl;
return 0;
}
扩展问题:
1.如果需要找出N个数中最大的K个不同的浮点数呢?比如,含有10个浮点数的数组(1.5,1.5,2.5,3.5,3.5,5,0,- 1.5,3.5)中最大的3个不同的浮点数是(5,3.5,2.5)。
解答:由前面的解法可以知道只有解法五要求是整数,因为计数数组count[]下标和这N个数数值有直接的一一对应关系,对其他解法改改都能用。
2. 如果是找第k到第m(0<k<=m<=n)大的数呢?
解答:一个很自然的方法是求解两个界k与m对应的值,然后扫描一次数组得到其内的所有值,一个更好的方法可以使用最小堆存储m个最大值,然后输出其内的m-k+1个,就得到结果了。考虑一个数组1 12 15 23 50 69 78 如果输出第2大到第4大的数23 50 69先建立容量为4的最小堆存放23 50 69 78,然后取出其内的较小的4-2+1=3个数。
3. 在搜索引擎中,网络上的每个网页都有“权威性”权重,如page rank。如果我们需要寻找权重最大的K个网页,而网页的权重会不断地更新,那么算法要如何变动以达到快速更新(incremental update)并及时返回权重最大的K个网页?
解答:用堆排序当每一个网页权重更新的时候,更新堆。更新堆是比较费时的,一个比较好的方法是采用映射二分堆,所谓映射二分堆是节点中不保存数据本身,而是保存一个映射,即指向数据单元的指针。并且可以根据索引删除一个元素。更新操作只需要O(logN)。
4. 在实际应用中,还有一个“精确度”的问题。我们可能并不需要返回严格意义上的最大的K个元素,在边界位置允许出现一些误差。当用户输入一个query的时候,对于每一个文档d来说,它跟这个query之间都有一个相关性衡量权重f (query, d)。搜索引擎需要返回给用户的就是相关性权重最大的K个网页。如果每页10个网页,用户不会关心第1000页开外搜索结果的“精确度”,稍有误差是可以接受的。比如我们可以返回相关性第10 001大的网页,而不是第9999大的。在这种情况下,算法该如何改进才能更快更有效率呢?网页的数目可能大到一台机器无法容纳得下,这时怎么办呢?
解答:由于网页数目大到一台机器无法容纳得下,所以可以让每台机器返回在这台机器中最相关的K’个文档,那么所有机器上各自最相关K’个文档的并集肯定包含全集中最相关的K个文档,依据归并排序的思想将之融合为K个文档。最好情况是K个文档在所有机器上均匀分布,K’ = K/n(n为所有机器总数);最坏情况一台机器1个最好文档,K’近似为K。
5. 如第4点所说,对于每个文档d,相对于不同的关键字q1, q2, …, qm,分别有相关性权重f(d, q1),f(d, q2), …, f(d, qm)。如果用户输入关键字qi之后,我们已经获得了最相关的K个文档,而已知关键字qj跟关键字qi相似,文档跟这两个关键字的权重大小比较靠近,那么关键字qi的最相关的K个文档,对寻找qj最相关的K个文档有没有帮助呢?
解答:肯定是有帮助的。 qi最相关的K个文档可能就是qj的最相关的K个文档,可以先假设这K个就是,然后根据问题四的解法获得K’,分别和这K个比较,可以用堆进行比较,从而获得qj最相关的K个文档。由于最开始的K个文档极有可能是最终的K个文档,所以K’和K比较的次数可能并不多。
参考:http://blog.csdn.net/rein07/article/details/6742933