一:寻找N个数中最大的K个数
这道题目比较经典,在多次面试题目中都见到过。此题理论上存在线性时间复杂度的算法,不过由于常数项太大,在实际应用过程中不怎么好。
下面的讨论跟存储无关,也就是说如果N很大,比如100亿,而无法一次装入内存,则可以分批装入。在这里还有个优化的地方就是可以一次尽量读入多的数,减少IO次数。
大部分人都推荐的做法是用堆,小根堆。下面具体解释下:
如果K = 1,那么什么都不需要做,直接遍历一遍,时间复杂度O(N)。
下面讨论K 比较大的情况,比如1万。
建立一个小根堆,则根是当前最小的第K个数。然后读入N-K个数,每次读入一个数就与当前的根进行比较,如果大于当前根,则替换之,并调整堆。如果小,则读入下一个。
时间复杂度O(N*logK)。本题还有一个时间复杂度比较好的做法。在编程之美上提到过该算法。
首先找到最大的第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) 递归对所有数据分成[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个数为止。先取出前100个数,维护一个100个数的最小堆,遍历一遍剩余的元素,在此过程中维护堆就可以了。具体步骤如下:
step1:取前m个元素(例如m=100),建立一个小顶堆。保持一个小顶堆得性质的步骤,运行时间为O(lgm);建立一个小顶堆运行时间为m*O(lgm)=O(m lgm);
step2:顺序读取后续元素,直到结束。每次读取一个元素,如果该元素比堆顶元素小,直接丢弃
如果大于堆顶元素,则用该元素替换堆顶元素,然后保持最小堆性质。最坏情况是每次都需要替换掉堆顶的最小元素,因此需要维护堆的代价为(N-m)*O(lgm);
最后这个堆中的元素就是前最大的10W个。时间复杂度为O(N lgm)。分块查找
先把100w个数分成100份,每份1w个数。先分别找出每1w个数里面的最大的数,然后比较。找出100个最大的数中的最大的数和最小的数,取最大数的这组的第二大的数,与最小的数比较
三:100亿个数字找出最大的10个
首先一点,对于海量数据处理,思路基本上是确定的,必须分块处理,然后再合并起来。
对于每一块必须找出10个最大的数,因为第一块中10个最大数中的最小的,可能比第二块中10最大数中的最大的还要大。
分块处理,再合并。也就是Google MapReduce 的基本思想。Google有很多的服务器,每个服务器又有很多的CPU,因此,100亿个数分成100块,每个服务器处理一块,1亿个数分成100块,每个CPU处理一块。然后再从下往上合并。注意:分块的时候,要保证块与块之间独立,没有依赖关系,否则不能完全并行处理,线程之间要互斥。另外一点,分块处理过程中,不要有副作用,也就是不要修改原数据,否则下次计算结果就不一样了。
上面讲了,对于海量数据,使用多个服务器,多个CPU可以并行,显著提高效率。对于单个服务器,单个CPU有没有意义呢?
也有很大的意义。如果不分块,相当于对100亿个数字遍历,作比较。这中间存在大量的没有必要的比较。可以举个例子说明,全校高一有100个班,我想找出全校前10名的同学,很傻的办法就是,把高一100个班的同学成绩都取出来,作比较,这个比较数据量太大了。应该很容易想到,班里的第11名,不可能是全校的前10名。也就是说,不是班里的前10名,就不可能是全校的前10名。因此,只需要把每个班里的前10取出来,作比较就行了,这样比较的数据量就大大地减少了。