寻找最小的K个数给你一堆无序的数,姑且假设都不相等,怎么找出其中最小的K个数呢? 首先想到的估计是从小到大排序,排序完了输出前K个数即可。基于比较的排序最快是O(nlgn),快排较好,输出的代价是O(k),总的时间复杂度就是T(n)=O(nlgn)+O(k)=O(nlgn)。 还有其他的方法吗?换句话说,我们有必要排序吗?我们只需要找到最小的K个数即可,你管他是否有序呢?前K个和后N-K个数都是没必要有序的。 先来看一个部分排序的,也就是前K个有序,后N-K个无序。这个时候选择排序(或者冒泡)就很好了,只需要一个大小为K的数组,经过K次遍历就可以得到最小的K个数了。首先找到k个数中的最大数kmax(kmax为k个元素的数组中最大元素),用时O(k),后再继续遍历后n-k个数,x与kmax比较:如果x<kmax,则x代替kmax,并再次重新找出k个元素的数组中最大元素kmax;如果x>kmax,则不更新数组。这样,每次更新或不更新数组的所用的时间为O(k)或O(0),总的时间复杂度平均下来为:T(n)=n*O(k)=O(n*k)。 至于这两种方法哪个更好一些,就要看lgn与k哪个更加的小了。 还有更好的办法吗?当然。其实刚才没必要用交换排序(选择,冒泡),我们可以用堆排序(要学以致用)。维护一个大小为K的最大堆,原理和交换排序一样。先遍历K得到K个数作为堆的元素,建堆用时O(K)(线性)。然后遍历后N-K个元素,更新堆用时O(lgK)或不更新堆O(0),总的时间复杂度是T(n)=O(K)+O((n-K)lgK)=O(nlgK)。很nice。(实现暂略) 可是,用堆的话我们也可以不这么做。我们可以直接对N个元素建最小堆,用时O(n)。然后维护,每次更新用时O(lgn),更新K次即可得到最小的K个数。总的时间复杂度是T(n)=O(n)+O(klgn)=O(n+klgn)。经证明,当n很大的时候,n+klgn<nlgk。也就是说这个时间复杂度更加的好。其实呢。。我们每次的更新没必要都是lgn,lgn是每次都要更新到堆低,其实完全没必要,我们又不是要排序,我们是要求最小的K个数而已。所以每次的更新只需要下降K次即可,这样0-k层是最小堆,下面的我们不管他。而且第一次下降K次,第二次只需下降K-1次即可,逐次减少,最后更新总的用时是O(k*k),总的时间复杂度是T(n)=O(n)+O(k^2)。比刚才的时间复杂度还好,如果K很小,那就可以达到线性的时间复杂度。
#include <iostream>
using namespace std;
const int N=100;
void Swap(int &a, int &b); //交换a b
void AdjustHeap(int array[],int start,int end); //调整最小堆
void BuildHeap(int array[],int start,int end); //建堆
int main()
{
int array[N+1];
int i,k;
k=10;
for(i=N;i>0;i--)
array[i]=i;
for(i=1;i<=N;i++)
cout<<array[i]<<" ";
cout<<endl;
BuildHeap(array,1,N); //初始建堆
swap(array[1],array[N]);
for(i=1;i<k;i++) //K次调整,每次 lgN
{
AdjustHeap(array,1,N-i);
swap(array[1],array[N-i]);
}
cout<<"最小的"<<k<<"个数:"<<endl;
for(i=N;i>N-k;i--)
cout<<array[i]<<" ";
cout<<endl;
return 0;
}
void AdjustHeap(int array[],int start,int end) //调整最小堆
{
int top;
top=array[start]; //top保存堆顶的值,不仅仅是整体,还有可能是子堆的堆顶
for(int j=2*start;j<=end;j=2*j) //数组下标从1开始
{
if(j<end&&array[j]>array[j+1])
{
j++;//右孩子小
}
if(array[j]<top)
{
array[start]=array[j]; ///让孩子覆盖父节点。
start=j; //此时让子节点作为新的父节点,即堆顶,继续调整
}
else
break;//不需要调整了,直接跳出
}
array[start]=top; //让最初的堆顶值放到合适的位置
}
void BuildHeap(int array[],int start,int end) //建堆
{
for(int i=end/2;i>=start;i--) //从最后一个非叶子节点开始调整
AdjustHeap(array,i,end);
}
void Swap(int &a, int &b) //交换a,b
{
if (a != b)
{
a ^= b;
b ^= a;
a ^= b;
}
}
-----------------------------------------------
int main()
{
int array[N+1];
int i,k;
k=10;
for(i=N;i>0;i--)
array[i]=i;
for(i=1;i<=N;i++)
cout<<array[i]<<" ";
cout<<endl;
BuildHeap(array,1,N); //初始建堆
swap(array[1],array[N]);
for(i=1;i<k;i++) //K次调整,保证K<lgN
{
AdjustHeap(array,1,int(pow(2,k-i+1)-1)); //持续调整,下降K次,每次递减
swap(array[1],array[N-i]);
}
cout<<"最小的"<<k<<"个数:"<<endl;
for(i=N;i>N-k;i--)
cout<<array[i]<<" ";
cout<<endl;
return 0;
}
可是你忘了最重要的一点了。内存占用太多,空间复杂度是O(n),如果上千亿的数据,你不就很拙计吗?内存放得下吗??还有,你要搞清楚空间复杂度和辅助空间的区别,这是原地排序,不需要辅助空间,可是空间复杂度是O(n)。空间复杂度是指运行完一个程序所需内存的大小,这里包括静态空间和动态空间以及递归栈所需的空间。所以来说,建堆占用内存很是重要,我们不常选择建立n个元素的最小堆取其K个,而是选择建立K个元素的最大堆,虽然时间复杂度高了一点点(O(nlgk)>O(n+k^2))(ps用1000W的数据测试发现差别真的不大),但是空间复杂度要好很多很多(O(k)<<O(n),尤其是N很大的时候优势就更加的明显)。(ps虽然我们经常是用数组来表示N个数据,可是数据大的时候我们就要用文件来处理了) 还有其他的方法吗?线性时间复杂度的?可以达到吗?计数排序,基数排序,桶排序。不是基于比较的排序,时间复杂度是O(n),输出前K个,时间复杂度就可以达到线性。不过,这个限制条件很多,比如计数排序,要保证所有的数保持在一定范围。如果所有的数都不相等,每一个数只需要一bit就可以表示了。Bloom filter,Bit-map。 还有没有其他的方法可以达到线性呢?有,有一种算法和快速排序很相像,是快速选择SELECT算法。N个数存储在数组S中,再从数组中选取一个枢纽数X,把数组划分为Sa和Sb两部分,Sa<=X<=Sb,如果要查找的k个元素小于Sa的元素个数,则返回Sa中较小的k个元素,否则返回Sa中k个小的元素+Sb中小的k-|Sa|个元素。这个要利用递归算法。 看起来时间复杂度好像是O(nlgn)??大错特错,这和快排是不一样的,快排的一次划分之后递归处理两边,而快速选择只处理划分的一边。这个算法的时间复杂度和划分的好坏是有关系的,如果划分是最差划分,那就拙计了,时间复杂度是O(n^2)。T(n)=T(n-1)+O(n)=O(n^2) (O(n)是划分的代价)。如果是最好划分,或者是有常数比例的划分,那就很好办了,T(n)=T(9n/10)+O(n)=O(n) (利用主方法很容易得到)。如果是随机划分,随机选择枢纽元素,则其期望时间为O(n),也就是平均时间复杂度。这个证明暂略。 RANDOMIZED-SELECT(A, p, r, i) //以线性时间做选择,目的是返回数组A[p..r]中的第i 小的元素 1 if p = r //p=r,序列中只有一个元素 2 then return A[p] 3 q ← RANDOMIZED-PARTITION(A, p, r) //随机选取的元素q作为主元 4 k ← q - p + 1 5 if i == k 6 then return A[q] //则直接返回A[q] 7 else if i < k 8 then return RANDOMIZED-SELECT(A, p, q - 1, i) //得到的k 大于要查找的i 的大小,则递归到低区间A[p,q-1]中去查找 9 else return RANDOMIZED-SELECT(A, q + 1, r, i - k) //得到的k 小于要查找的i 的大小,则递归到高区间A[q+1,r]中去查找。 注意我们要求的是最小的K个数,不是第K小的数,虽然实质上是一样的,但是此时我们编写代码要返回第K小的下标,划分带来的好处是K前面的都是小于K的,顺序输出即可,虽然找到第K小的数,遍历一遍即可,不过还是返回下标比较好啦。 Ok,编码实现一下
#include <iostream>
#include<math.h>
#include<time.h>
using namespace std;
const int N=10;
void Swap(int &a, int &b); //交换a b
int RandomPartition(int array[],int low,int high); //随机选择枢纽
int MyRandom(int low, int high) ; //随机函数
int RandomSelect(int array[],int low,int high,int k);
int main()
{ int array[N+1];
int i,k;
k=10;
for(i=N;i>0;i--)
array[i]=N-i+1;
for(i=1;i<=N;i++)
cout<<array[i]<<" ";
cout<<endl;
k=RandomSelect(array,1,N,k);
for(i=1;i<=k;i++)
cout<<array[i]<<" ";
return 0;
}
int MyRandom(int low, int high)
{
srand(time(0));
int size = high - low + 1;
return low + rand() % size;
}
int RandomPartition(int array[],int low,int high) //随机选择枢纽划分
{
int privot;
int i=MyRandom(low,high);
swap(array[low],array[i]);
privot=array[low];
while(low<high)
{
while(low<high&&privot<=array[high])
high--;
if(low<high)
{
array[low]=array[high];
low++;
}
while(low<high&&array[low]<=privot)
low++;
if(low<high)
{
array[high]=array[low];
high--;
}
}
array[low]=privot;
return low;
}
int RandomSelect(int array[],int low,int high,int k) //随机快速选择算法
{
if(k<1||k>high-low+1)
return -1; //错误返回
if(low==high)
return low;
int pivot=RandomPartition(array,low,high);
int m=pivot-low+1; //
if(m==k)
return pivot;
else if(m>k)
return RandomSelect(array,low,pivot-1,k);
else
return RandomSelect(array,pivot+1,high,k-m);
}
不用随机划分也可,利用三数取中法也可以达到平均时间复杂度是O(n)的程度。 center = (left + right) / 2; if( a[left] > a[center] ) swap( &a[left], &a[center] ); if( a[left] > a[right] ) swap( &a[left], &a[right] ); if( a[center] > a[right] ) swap( &a[center], &a[right] ); 不过有一种划分方法可以在最坏的情况达到线性时间复杂度。 “五分化中项的中项”划分法: 1 将输入数组的N个元素划分为[n/5]组,且至多只有一个组有剩下的n mod5组成。 2 寻找这个[n/5]组中没一组的中位数:首先对每组的元素进行插入排序,排序后选出中位数。 3 对第二步找出的[n/5]个中位数,继续递归找到其中位数x。 4 按中位数的中位数x进行partition划分,然后就是select算法。 可以证明的是该划分可以在最坏情况下保证O(n)的时间复杂度。 上图:n个元素由小圆圈来表示,并且每一个组占一纵列。组的中位数用白色表示,而各中位数的中位数x也被标出。(当寻找偶数数目元素的中位数时,使用下中位数)。箭头从比较大的元素指向较小的元素,从中可以看出,在x的右边,每一个包含5个元素的组中都有3个元素大于x,在x的左边,每一个包含5个元素的组中有3个元素小于x。大于x的元素以阴影背景表示。 这样在一半的组中除了元素少于5个的那组和包含x的那组,其他的至少有3个元素大于x。也即是说至少有3((1/2)*(n/5)-2))个数大于x,3((1/2)*(n/5)-2))>=3n/10-6,同理小于x的数至少也有3n/10-6.那么大于x或者小于x的数至多有7n/10+6,也即是说递归select最多有7n/10+6个元素进行递归调用。求中位数的中位数用时O([n/5]),划分用时O(n) ,则时间复杂度 T(n)=O([n/5])+O(7n/10+6)+O(n)<=cn/5+c+7cn/10+6c+an=9cn/10+7cn+an=cn+(-cn/10+7c+an) 如果-cn/10+7c+an<=0,也就是n>70,则T(n)=O(n).编码实现:
#include <iostream>
#include<math.h>
#include<time.h>
using namespace std;
const int N=10;
int median[N/5+1];
void Swap(int &a, int &b); //交换a b
void InsertionSort(int array[],int low,int high);//插入排序
int FindMedian(int array[],int low,int high); //找到中位数的中位数
int FindIndex(int array[], int low, int high, int median); //找到中位数的中位数所在下标
int MedianPartition(int array[],int low,int high);
int MedianSelect(int array[],int low,int high,int k);
int main()
{
int array[N+1];
int i,k;
k=4;
for(i=N;i>=0;i--)
array[i]=N-i+1;
for(i=0;i<=N;i++)
cout<<array[i]<<" ";
cout<<endl;
k=MedianSelect(array,0,N-1,k);
for(i=0;i<=k;i++)
cout<<array[i]<<" ";
return 0;
}
void InsertionSort(int array[],int low,int high)//插入排序
{
int key;
for (int i=low+1;i<=high;i++)
{
key=array[i];
for(int j=i-1;j>=low&&array[j]>key;j--) //注意细节是>=low和>key
{
array[j+1]=array[j];
}
array[j+1]=key;
}
}
int FindMedian(int array[],int low,int high) //找到中位数的中位数
{
if(low==high)
return array[low];
int num=0;
for(int i=low;i<=high-4;i+=5) //
{
InsertionSort(array,i,i+4);
num=i-low;
median[num/5]=array[i+2];
}
int LeftNum=high-i+1; //可能遗留的数,不足5个/
if( LeftNum>0)
{
InsertionSort(array,i-5,high);
num=i-5-low;
median[num/5]=array[(i-5)+ LeftNum/2];
}
int MedianNum=(high-low+1)/5;
if((high-low)%5!=0)
MedianNum++;
if(1==MedianNum)
return median[0]; //返回中位数的中位数
else
return FindMedian(median,0,MedianNum-1); //下标从0开始
}
int FindIndex(int array[], int low, int high, int median) //找到中位数的中位数所在下标
{
for (int i=low; i<=high; i++)
{
if (array[i]==median)
return i;
}
return -1;
}
int MedianPartition(int array[],int low,int high) //五分化中项的中项的划分
{
int privot;
int i=FindIndex(array,low,high,FindMedian(array,low,high));
swap(array[low],array[i]);
privot=array[low];
while(low<high)
{
while(low<high&&privot<=array[high])
high--;
if(low<high)
{
array[low]=array[high];
low++;
}
while(low<high&&array[low]<=privot)
low++;
if(low<high)
{
array[high]=array[low];
high--;
}
}
array[low]=privot;
return low;
}
int MedianSelect(int array[],int low,int high,int k) //五分化中项的中项快速选择算法
{
if(k<1||k>high-low+1)
return -1; //错误返回
if(low==high)
return low;
int pivot=MedianPartition(array,low,high);
int m=pivot-low+1; //
if(m==k)
return pivot;
else if(m>k)
return MedianSelect(array,low,pivot-1,k);
else
return MedianSelect(array,pivot+1,high,k-m);
}
Ok,这个问题暂时告一段落,后续还会有整理。 转载请注明出处http://blog.csdn.net/sustliangbo/article/details/9377105