寻找最小的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