寻找N个数中最大的K个数整理

一:寻找N个数中最大的K个数

这道题目比较经典,在多次面试题目中都见到过。此题理论上存在线性时间复杂度的算法,不过由于常数项太大,在实际应用过程中不怎么好。
下面的讨论跟存储无关,也就是说如果N很大,比如100亿,而无法一次装入内存,则可以分批装入。在这里还有个优化的地方就是可以一次尽量读入多的数,减少IO次数。

  1. 大部分人都推荐的做法是用堆,小根堆。下面具体解释下:
    如果K = 1,那么什么都不需要做,直接遍历一遍,时间复杂度O(N)。
    下面讨论K 比较大的情况,比如1万。
    建立一个小根堆,则根是当前最小的第K个数。然后读入N-K个数,每次读入一个数就与当前的根进行比较,如果大于当前根,则替换之,并调整堆。如果小,则读入下一个。
    时间复杂度O(N*logK)。

  2. 本题还有一个时间复杂度比较好的做法。在编程之美上提到过该算法。
    首先找到最大的第K个数。这个时间复杂度可以做到O(N),具体做法如下:
    从N个数中随机选择一个数,扫描一遍,比n大的放在右边,r个元素,比n小的放左边,l个元素
    如果: a:l = K-1 返回n
    b:l > K-1 在l个元素中继续执行前面的操作。
    c:l < K-1 在r个元素中继续执行前面的操作。
    b,c每次只需执行一项,因此平均复杂度大概为:O(n+n/2+n/4…)=O(2n)=O(n)

这一步类似快速排序里面的步骤。

接下来,选择比n大的数即可,如果不足,用K填上。
总的复杂度依然是O(n)

int RandomSelect(int a[], int left, int right, int k)
{
        int i,j,p;
        if (right <= 1) return a[right];
        i = RandomPartition(a[], left, right);
        /************************************************
        * RandomPartition,把a[left:right]随机划分为:
        * a[left : i-1] <= a[i] <= a[i+1 : right].
        *************************************************/
        j = right - i + 1;
        /* j 为 a[i : right] 的元素个数*/
        if (j == k) return a[i];
        if (j > k)
            /* 第k大的数在右子数组 */
            return RandomSelect(a, i+1, right, k);
        else 
            /* 第k大的数在左子数组 */
            return RandomSelect(a, left, i-1 , k-j);
}

如果数据很大,那么这个做法就没有那么完美了,因为数组要保持部分有序。而如果数据很大,内存无法保存数组的某种状态的话就不行了。

举个例子
100亿个数,求最大的1万个数,并说出算法的时间复杂度。

考虑空间情况下
把100亿个数分成1000个子集,每个子集1000万个数,对每个子集进行堆排序求出1万最大的数,然后把1000个子集中的所有的1万个最大数合并成起来,型成一个1000万的集合,再进行堆排序。求出1万个最大数。

时间复杂度为
n=1000万
O(1001(nlogn))

二:100万个数中找出最大的前100个数

  1. 算法如下:根据快速排序划分的思想
    (1) 递归对所有数据分成[a,b)b(b,d]两个区间,(b,d]区间内的数都是大于[a,b)区间内的数
    (2) 对(b,d]重复(1)操作,直到最右边的区间个数小于100个。注意[a,b)区间不用划分
    (3) 返回上一个区间,并返回此区间的数字数目。接着方法仍然是对上一区间的左边进行划分,分为[a2,b2)b2(b2,d2]两个区间,取(b2,d2]区间。如果个数不够,继续(3)操作,如果个数超过100的就重复1操作,直到最后右边只有100个数为止。

  2. 先取出前100个数,维护一个100个数的最小堆,遍历一遍剩余的元素,在此过程中维护堆就可以了。具体步骤如下:
    step1:取前m个元素(例如m=100),建立一个小顶堆。保持一个小顶堆得性质的步骤,运行时间为O(lgm);建立一个小顶堆运行时间为m*O(lgm)=O(m lgm);
    step2:顺序读取后续元素,直到结束。每次读取一个元素,如果该元素比堆顶元素小,直接丢弃
    如果大于堆顶元素,则用该元素替换堆顶元素,然后保持最小堆性质。最坏情况是每次都需要替换掉堆顶的最小元素,因此需要维护堆的代价为(N-m)*O(lgm);
    最后这个堆中的元素就是前最大的10W个。时间复杂度为O(N lgm)。

  3. 分块查找
    先把100w个数分成100份,每份1w个数。先分别找出每1w个数里面的最大的数,然后比较。找出100个最大的数中的最大的数和最小的数,取最大数的这组的第二大的数,与最小的数比较

三:100亿个数字找出最大的10个

  1. 首先一点,对于海量数据处理,思路基本上是确定的,必须分块处理,然后再合并起来。

  2. 对于每一块必须找出10个最大的数,因为第一块中10个最大数中的最小的,可能比第二块中10最大数中的最大的还要大。

  3. 分块处理,再合并。也就是Google MapReduce 的基本思想。Google有很多的服务器,每个服务器又有很多的CPU,因此,100亿个数分成100块,每个服务器处理一块,1亿个数分成100块,每个CPU处理一块。然后再从下往上合并。注意:分块的时候,要保证块与块之间独立,没有依赖关系,否则不能完全并行处理,线程之间要互斥。另外一点,分块处理过程中,不要有副作用,也就是不要修改原数据,否则下次计算结果就不一样了。

  4. 上面讲了,对于海量数据,使用多个服务器,多个CPU可以并行,显著提高效率。对于单个服务器,单个CPU有没有意义呢?

      也有很大的意义。如果不分块,相当于对100亿个数字遍历,作比较。这中间存在大量的没有必要的比较。可以举个例子说明,全校高一有100个班,我想找出全校前10名的同学,很傻的办法就是,把高一100个班的同学成绩都取出来,作比较,这个比较数据量太大了。应该很容易想到,班里的第11名,不可能是全校的前10名。也就是说,不是班里的前10名,就不可能是全校的前10名。因此,只需要把每个班里的前10取出来,作比较就行了,这样比较的数据量就大大地减少了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值