找出最大的K个数问题

看到说找工作笔试面试经常会出这道题,题目很简单明了,就是在一组数字中,找出最大的K个数,首先想到的就是利用快排算法改一改。

 这里简单回顾下快排算法(从小到大):

  1
  2 int Partition(int a[],int low,int high)
  3 {
  4     int pivotkey;
  5     pivotkey=a[low];
  6     while(low<high)
  7     {
  8         while(low<high&&a[high]>=pivotkey)
  9         {
 10             --high;
 11         }   
 12         a[low]=a[high];
 13         while(low<high&&a[low]<=pivotkey)
 14         {
 15             ++low;
 16         }   
 17         a[high]=a[low];
 18     }   
 19     a[low]=pivotkey;
 20     return low;
 21 }   
 22 
 23 void QSort(int a[], int low, int high)
 24 {
 25     int pivotloc;
 26     if(low < high)
 27     {
 28         pivotloc = Partition(a, low, high);
 29         QSort(a, low, pivotloc-1);
 30         QSort(a, pivotloc+1, high);
 31     }   
 32 }   

因为我们只需要找出最大的K个数,不要求排序,所以在QSort()里不用再对pivotloc的两边都分别在递归执行QSort(),
而是把pivotloc左半边的长度L(即大于等于a[pivotloc]的数的个数)与K进行比较:
若L>K,即左半边个数大于K,需要在左半边找出K个
若L<K,即左半边个数小于K,需要在右半边找出K-L个
另外对于Partition()比较大小那里的“大于号“”小于号”要调换一下,因为上面那个是从小到大排,而现在要从大到小排。

  1 #include <stdio.h>
  2 #define K 6
  3 
  4 int Partition(int a[],int low,int high)
  5 {
  6     int pivotkey;
  7     pivotkey=a[low];
  8     while(low<high)
  9     {
 10         while(low<high&&a[high]<=pivotkey)
 11         {
 12             --high;
 13         }
 14         a[low]=a[high];
 15         while(low<high&&a[low]>=pivotkey)
 16         {
 17             ++low;
 18         }
 19         a[high]=a[low];
 20     }
 21     a[low]=pivotkey;
 22     return low;
 23 }
 24 
 25 void findKNums(int a[], int low, int high, int kValue)
 26 {
 27     if(kValue <= 0)
 28         return;
 29     if(low < high){
 30         int pivotloc = Partition(a,low,high);
 31         int aLen = pivotloc-low+1;
 32         if( aLen ==  kValue)
 33             return;
 34         else if(aLen < kValue)
 35             findKNums(a,pivotloc+1,high,kValue-aLen);
 36         else
 37             findKNums(a,low,pivotloc-1,kValue);
 38     }
 39 }
 40 
 41 int main()
 42 {
 43     int nums[9] = {20,13,2,28,22,24,19,1,3};
 44     findKNums(nums, 0, 8, K);
 45 
 46     for(int i=0; i<K; i++){
 47         printf("%d ", nums[i]);
 48     }
 49     printf("\n");
 50     return 0;
 51 }

写起来的话,要修改的地方其实不多,小小地验证了下,应该没错了。后来想想好像有些细节没处理,比如又相同大小的值的取舍,原序列长度比K小等,
懒得改了,然后又在网上搜索一番,想看看其他解法,发现这道题还在《编程之美》里2.5,哇咔咔,解法能不多咩~~
解法一:用快排或者堆排把整个序列都排序,然后去前K个值,时间复杂度ON * log2N
解法二:用快排只做前K个数排序,避免做后N-K个数的排序,跟我上面误打误撞做的差不多,平均时间复杂度ON * log2K
解法三:用二分搜索找出第K大的数,时间复杂度ON * log2N),然后在O(N)时间复杂度里找出所有大于第K大数的数

#include <stdio.h>
#define N 9
#define K 6 
#define DELTA 0.5
//delta的取值要比所有数中的任意两个不相等的元素差值之最小值小
//如果所有元素都是整数,delta可以取值0.5

int countNums(int b[], float Mvalue)
{
    int Mcount = 0; 
    for(int j=0; j<N; j++)
        if((float)b[j] >= Mvalue)
            Mcount++;
    return Mcount;
}

void findKNums(int a[])
{
    float Vmin=(float)a[0],Vmax=(float)a[0],Vmid;
    for(int i=1; i<N; i++){
        if(a[i] < Vmin)
            Vmin = (float)a[i];
        if(a[i] > Vmax)
            Vmax = (float)a[i];
    }    
    while(Vmax - Vmin > DELTA){                                  
        Vmid = (Vmax + Vmin)/2;    
        if(countNums(a, Vmid) >= K)
            Vmin = Vmid;
        else
            Vmax = Vmid;
    }    
    for(int k=0; k<N; k++)
        if((float)a[k] >= Vmin)
            printf("%d ",a[k]);
    printf("\n");
} 

int main()
{
    int nums[N] = {20,13,2,28,22,24,19,1,3};  
    findKNums(nums);   
    return 0;    
}

解法四:用一个数组来存储最大的K个数,每新加入一个数X如果X比最大的K个数中的最小的数Y小,那么最大的K个数还是保持不变。如果XY大,那么最大的K个数应该去掉Y,而包含X,所耗费的时间为ON * K)。进一步,可以用容量为K的最小堆来存储最大的K个数,更新过程花费的时间复杂度为O(log2K

#include <stdio.h>
#define N 9
#define K 6 

void adjustMinHeap(int h[], int pos)
{
    int t,p,q;
    p = pos;    
    while(p < K){
        q = 2*p+1;
        if(q >= K)
            break;
        if((q < K-1) && (h[q+1] < h[q]))
            q = q+1;
        if( h[q] < h[p]){
            t = h[p];
            h[p] = h[q];
            h[q] = t;
            p = q;
        }
        else
            break;
    }
    
}

void sortMinHeap(int a[])
{
    int i;
    for(i = K/2-1; i>= 0; i--)
        adjustMinHeap(a, i);
    for(i = K; i<N; i++){
        if(a[i] > a[0]){
            a[0] = a[i];
            adjustMinHeap(a,0);
        }
    }
}

int main()
{
    int nums[N] = {20,13,2,28,22,24,19,1,3};  
    sortMinHeap(nums);
    for(int i=0; i<K; i++)
        printf("%d ", nums[i]);
    printf("\n");
    return 0;    
}

解法五:如果所有N个数都是正整数,且它们的取值范围不太大,可以考虑申请空间,记录每个整数出现的次数,然后再从大到小取最大的K个。比如,所有整数都在(0, MAXN)区间中的话,利用一个数组count[MAXN]来记录每个整数出现的个数(count[i]表示整数i在所有整数中出现的个数)。我们只需要扫描一遍就可以得到count数组。然后,寻找第K大的元素。
      当实际情况下,并不一定能保证所有元素都是正整数,且取值范围不太大。上面的方法仍然可以推广适用。如果N个数中最大的数为Vmax,最小的数为Vmin,我们可以把这个区间[VminVmax]分成M块,每个小区间的跨度为d =(Vmax – Vmin)/M,即 [VminVmin+d], [Vmin + dVmin + 2d],……然后,扫描一遍所有元素,统计各个小区间中的元素个数,跟上面方法类似地,我们可以知道第K大的元素在哪一个小区间。然后,再对那个小区间,继续进行分块处理。这个方法介于解法三和类计数排序方法之间,不能保证线性。跟解法三类似地,时间复杂度为O((N+M)* log2M(|Vmax - Vmin|/delta))。遍历文件的次数为2 * log2M(|Vmax Vmin|/delta)。当然,我们需要找一个尽量大的M,但M取值要受内存限制。

#include <stdio.h>
#define N 9
#define K 6 
#define MAXN 28//为了测试方便随便填的

void findKNums(int a[])
{
    int i,j,sumCount,v;
    int count[MAXN+1] = {0};
    for(i = 0; i<N; i++)
        count[a[i]]++;
    for(i = 0; i<=MAXN; i++)
        printf("%d %d \n",i,count[i]);
    for(sumCount = 0, v = MAXN; v >= 0; v--){
        sumCount += count[v];
        if(sumCount >= K)
            break;
    }
    sumCount = 0;
    for(i = MAXN; i>=v; i--){
        if(count[i] != 0)
            for(j = count[i]; j>0; j--){
                printf("%d ",i);
                sumCount++;
                if(sumCount == K)
                    break;
            }
    }
}

int main()
{
    int nums[N] = {20,13,13,28,22,24,13,1,3};  
    findKNums(nums);
    printf("\n");
    return 0;    
}

 上面五个解法摘自《编程之美》,丑丑的代码是不才根据书中算法写的。这真是本好书,看来要买一本回来支持下正版。。。

posted @ 2013-02-28 22:59  sillypudding  阅读(387)  评论(0编辑  收藏  举报