寻找最大的K个数

寻找最大的K个数

解法1:在元素数量不大的情况下,采用快排或者堆排序对所有元素排序,取前K个,时间复杂度为O( N*logN )+O( K )= O( N*logN ); 采用部分排序算法,如选择排序或交换排序,把N个数中的前K个数排序出来,复杂度为O( N*K ); 具体选择取决于KlogN的大小。

解法2:按照快速排序的思路,假设N个数存储在数组S中,从数组S中随机找出一个元素X,把数组分为两部分SaSbSa中的元素大于等于XSb中的元素小于X。这时有两种可能:

1.Sa中的元素的个数小于KSa中所有的数和Sb中最大的K-|Sa||Sa|Sa中元素的个数)个元素就是数组S中最大的K个数。

2.Sa中元素的个数大于或等于K,则需要返回Sa中最大的K个元素。

如此递归。平均时间复杂度为O( N*logK )。伪代码如下:

Kbig( S, K )

    if ( k <= 0 ):

       return []

    if ( S. length <= K ):

       return S

    ( Sa,Sb ) = Partition( S )

    return Kbig( Sa, K ).Append( Kbig( Sb, K - Sa.length ))

Partition(S):

     Sa=[];

     Sb=[];

     Swap( s[1], S[random() % S. length] )

     p=S[1]

     for i in [2: S.length]:

         S[i] > p ? Sa.Append( S[i] ):Sb.Append( S[i] )

Sa.length < Sb.length ? Sa.Append(p):Sb.Append(p)

return (Sa,Sb)

解法3:寻找N个数中最大的K个数,本质上就是寻找最大的K个数中最小的那个,也就是K大的数。可以使用二分搜索的策略来寻找N个数中的第K大的数。然后,对于一个给定的数p,可以在O(N)的时间复杂度内找出所有不小于p的数。

假如N个数中最大的数为Vmax,最小的数为Vmin,那么这N个数中的第K大数一定在区间[Vmin, Vmax]之间。那么,可以在这个区间内二分搜索N个数中的第K大数p。伪代码如下:

while(Vmax – Vmin > delta)

{

     Vmid = Vmin + (Vmax - Vmin) * 0.5;

     if( f(arr, N, Vmid) >= K)

         Vmin = Vmid;

     else

         Vmax = Vmid;

     }

伪代码中f(arr, N, Vmid)返回数组arr[0, …, N-1]中大于等于Vmid的数的个数。

上述伪代码中,delta的取值要比所有N个数中的任意两个不相等的元素差值之最小值小。如果所有元素都是整数,delta可以取值0.5。循环运行之后,得到一个区间(Vmin, Vmax),这个区间仅包含一个元素(或者多个相等的元素)。这个元素就是第K大的元素。整个算法的时间复杂度为O(N * log2(|Vmax - Vmin| /delta))。由于delta的取值要比所有N个数中的任意两个不相等的元素差值之最小值小,因此时间复杂度跟数据分布相关。在数据分布平均的情况下,时间复杂度为O(N * log2(N))。

解法4:当N非常大的情况下,不能一次性装入内存操作。设N > K,前K个数中的最大K个数是一个退化的情况,所有K个数就是最大的K个数。考虑第K+1个数X。如果X比最大的K个数中的最小的数Y小,那么最大的K个数还是保持不变。如果X比Y大,那么最大的K个数应该去掉Y,而包含X。如果用一个数组来存储最大的K个数,每新加入一个数X,就扫描一遍数组,得到数组中最小的数Y。用X替代Y,或者保持原数组不变。这样的方法,所耗费的时间为O(N * K)。

进一步,可以用容量为K的最小堆来存储最大的K个数。最小堆的堆顶元素就是最大K个数中最小的一个。每次新考虑一个数X,如果X比堆顶的元素Y小,则不需要改变原来的堆,因为这个元素比最大的K个数小。如果X比堆顶元素大,那么用X替换堆顶的元素Y。在X替换堆顶元素Y之后,X可能破坏最小堆的结构(每个结点都比它的父亲结点大),需要更新堆来维持堆的性质。更新过程花费的时间复杂度为O(log2K)。

解法5:可以通过改进计数排序、基数排序等来得到一个更高效的算法。但算法的适用范围会受到一定的限制。如果所有N个数都是正整数,且它们的取值范围不太大,可以考虑申请空间,记录每个整数出现的次数,然后再从大到小取最大的K个。比如,所有整数都在(0, MAXN)区间中的话,利用一个数组count[MAXN]来记录每个整数出现的个数(count[i]表示整数i在所有整数中出现的个数)。我们只需要扫描一遍就可以得到count数组。然后,寻找第K大的元素:

for( sumCount = 0, v = MAXN – 1;  v >= 0;  v-- )

{

     sumCount += count[v];

     if( sumCount >= K )

         break;

}

return v;

极端情况下,如果N个整数各不相同,我们甚至只需要一个bit来存储这个整数是否存在。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值