寻找最小的K个数

来源:互联网 发布:东华软件股票怎么样 编辑:程序博客网 时间:2024/06/09 14:21
寻找最小的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 bint 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



原创粉丝点击