题目:取出10000个数字中最大的500个,怎么做?
如果要解决100亿个数呢?这时数据不能全部装入内存,所以要尽可能少的遍历所有数据。
可以用容量为K的最小堆来存储最大的K个数。最小堆的堆顶元素就是最大K个数中最小的一个。每次新考虑一个数X,如果X比堆顶的元素Y小,则不需要改变原来的堆,因为这个元素比最大的K个数小。如果X比堆顶元素大,那么用X替换堆顶元素Y。在X替换堆顶元素Y之后,X可能破换最小堆的结构。(每个结点都比它的父亲结点大),需要更新堆来维持堆的性质。更新过程花费的时间复杂度为O(log2K)。
#include<stdio.h>
#define N 10001
#define K 5
void swap(int *a,int *b)
{
*a = *a + *b;
*b = *a - *b;
*a = *a - *b;
}
void print(int *a,int n)
{
int i;
for(i=1;i<n;i++)
printf("%8d",a[i]);
printf("\n");
}
void min_heapadjust(int *a,int i,int size)
{
int l = 2*i;
int r = 2*i + 1;
int min = i;
if(l<=size && a[l]<a[i])
min = l;
if(r<=size && a[r]<a[min])
min = r;
if(min != i)
{
swap(&a[i],&a[min]);
min_heapadjust(a,min,size);
}
}
void build_heap(int *a,int size)
{
int i;
for(i=size/2;i>=1;i--)
min_heapadjust(a,i,size);
}
void topK(int *a,int size)
{
build_heap(a,size);
int i;
for(i=1;i<N;i++)
{
if(a[i]>a[1])
{
swap(&a[i],&a[1]);
min_heapadjust(a,1,size);
}
}
}
int main(int argc,char *argv[])
{
int a[N];
int i;
for(i=1;i<N;i++)
a[i] = i;
print(a,N);
topK(a,K);
print(a,K+1);
return 0;
}
因此,算法只需要扫描所有的数据一次,时间复杂度为 O(N * log2K)。这实际上是部分执行了堆排序的算法。在空间方面,由于这个算法只扫描所有的数据一次,因此我们只需要存储一个容量为 K 的堆。大多数情况下,堆可以全部载入内存。如果 K 仍然很大,我们可以尝试先找最大的 K’个元素,然后找第 K’+1个到第 2 * K’个元素,如此类推(其中容量 K’的堆可以完全载入内存)。不过这样,我们需要扫描所有数据 ceil1(K/K’)次。
现在我介绍一种线性算法,但算法的范围会受到一定的限制。
如果所有 N 个数都是正整数,且它们的取值范围不太大,可以考虑申请空间,记录每个整数出现的次数,然后再从大到小取最大的 K 个。比如,所有整数都在(0, MAXN)区间中的话,利用一个数组 count[MAXN]来记录每个整数出现的个数(count[i]表示整数 i 在所有整数中出现的个数)。我们只需要扫描一遍就可以得到 count 数组。然后,寻找第 K 大的元素:
#include<stdio.h>
#define N 10000
#define K 5
#define MAX 20000
void print(int *a,int n)
{
int i;
for(i=0;i<n;i++)
printf("%8d",a[i]);
printf("\n");
}
int count[MAX] = {0};
int topK(int *a,int max)
{
int i;
int sum = 0;
for(i=0;i<N;i++)
count[a[i]]++;
for(i=max;i>=0;i--)
{
sum += count[i];
if(sum >= K)
break;
}
return i;
}
int main(int argc,char *argv[])
{
int a[N];
int i;
for(i=0;i<N;i++)
a[i] = i;
print(a,N);
i = topK(a,MAX);
int m = MAX-1,j;
for(j=0;;)
{
while(count[m]--)
{
printf("%8d",m);
j++;
if(j>=K)
return 0;
}
m--;
}
return 0;
}