【经典问题】1亿个数据取前1万大的整数
数据规模分析
不考虑操作系统的区别,通常将C++中的一个整型变量认为4bytes。那么1亿整型需要400M左右的内存空间。当然,就现代PC机而言,连续开辟400M的内存空间还是可行的。因此,下面的讨论只考虑在内存中的情况。为了讨论方便,假设M=1亿,N=1万。
用大拇指想想
略微考虑一下,使用选择排序。循环1万次,每次选择最大的元素。源代码如下:
- //解决方案1,简单选择排序
- //BigArr[]存放1亿的总数据、ResArr[]存放1万的总数据
- void solution_1(int BigArr[], int ResArr[] ){
- for( int i = 0; i < RES_ARR_SIZE; ++i ){
- int idx = i;
- //选择最大的元素
- for( int j = i+1; j < BIG_ARR_SIZE; ++j ){
- if( BigArr[j] > BigArr[idx] )
- idx = j;
- }
- //将最大元素交换到开始位置
- ResArr[i] = BigArr[idx];
- std::swap( BigArr[idx], BigArr[i] );
- }
- }
性能分析: 哇靠!时间复杂度为O(M*N)。 有人做过实验《从一道笔试题谈算法优化(上) 》,需要40分钟以上的运行时间。太悲剧了......
当然,用先进的排序方法(比如快排),时间复杂度为O(M*logM)。虽然有很大的改进了,据说使用C++的STL中的快排方法只需要32秒左右。确实已经达到指数级的优化了,但是否还能够优化呢?
稍微动下脑子
我们只需要1万个最大的数,并不需要所有的数都有序,也就是说只要保证的9999万个数比这1万个数都小就OK了。我们可以通过下面的方法来该进:
(1) 先找出M数据中的前N个数。确定这N个数中的最小的数MinElement。
(2) 将 (N+1) —— M个数循环与MinElement比较,如果比MinElement还小,则不处理。如果比MinElement大,则与MinElement交换,然后重新找出N个数中的MinElement。
- //解决方案2
- void solution_2( T BigArr[], T ResArr[] ){
- //取最前面的一万个
- memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );
- //标记是否发生过交换
- bool bExchanged = true;
- //遍历后续的元素
- for( int i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i ){
- int idx;
- //如果上一轮发生过交换
- if( bExchanged ){
- //找出ResArr中最小的元素
- int j;
- for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j ){
- if( ResArr[idx] > ResArr[j] )
- idx = j;
- }
- }
- //这个后续元素比ResArr中最小的元素大,则替换。
- if( BigArr[i] > ResArr[idx] ){
- bExchanged = true;
- ResArr[idx] = BigArr[i];
- }else
- bExchanged = false;
- }
- }
性能分析: 最坏的时间复杂度为O((M-N)*N)。咋一看好像比快排的时间复杂度还高。但是注意是最坏的,实际上,并不是每次都需要付出一个最小值O(N)的代价的。因为,如果当前的BigArr[i]<ResArr[idx]的话,就不需要任何操作,则1——N的最小值也就没有变化了。下一次也就不需要付出O(N)的代价去寻找最小值了。当然, 如果M基本正序的话,则每次都要交换最小值,每次都要付出一个O(N)代价。最坏的情况比快排还要差。
就平均性能而言,改进的算法还是比快排要好的,其运行时间大约在2.0秒左右。
使劲动下脑子
上面的解决方案2还有一个地方不太好。当BigArr[i]>ResArr[idx]时,则必须交换这两个数,进而每次都需要重新计算一轮N个数的最小值。只改变了一个数就需要全部循环一次N实在是不划算。能不能下一次的最小值查找可以借助上一次的比较结果呢?
基于这样一个想法,我们考虑到了堆排序的优势(每一次调整堆都只需要比较logN的结点数量)。因此我们再做一次改进:
(1) 首先我们把前N个数建立成小顶堆,则根结点rootIdx。
(2) 当BigArr[i]>ResArr[rootIdx]时,则交换这两个数,并重新调整堆,使得根结点最小。
性能分析:显然,除了第一次建堆需要O(N)时间的复杂度外,每一次调整堆都只需要O(logN)的时间复杂度。因此最坏情况下的时间复杂度为O((M-N)*logN),这样即使在最坏情况下也比快排的O(M*logM)要好的多了。
另外:实际上也可以使用二分查找的思想,第一次找N中的最小值的时候将N排序。以后每次替换最小值,都使用二分查找在logN代价下找到当前N的最小值即可。与使用堆的过程如出一辙。