【经典问题】1亿个数据取前1万大的整数

数据规模分析

 

不考虑操作系统的区别,通常将C++中的一个整型变量认为4bytes。那么1亿整型需要400M左右的内存空间。当然,就现代PC机而言,连续开辟400M的内存空间还是可行的。因此,下面的讨论只考虑在内存中的情况。为了讨论方便,假设M=1亿,N=1万。

 

 

用大拇指想想

略微考虑一下,使用选择排序。循环1万次,每次选择最大的元素。源代码如下:

Cpp代码  收藏代码
  1. //解决方案1,简单选择排序  
  2. //BigArr[]存放1亿的总数据、ResArr[]存放1万的总数据  
  3. void solution_1(int BigArr[], int ResArr[] ){  
  4.        forint i = 0; i < RES_ARR_SIZE; ++i ){  
  5.               int idx = i;  
  6.               //选择最大的元素  
  7.               forint j = i+1; j < BIG_ARR_SIZE; ++j ){  
  8.                      if( BigArr[j] > BigArr[idx] )  
  9.                             idx = j;  
  10.               }  
  11.               //将最大元素交换到开始位置  
  12.               ResArr[i] = BigArr[idx];  
  13.               std::swap( BigArr[idx], BigArr[i] );  
  14.        }  
  15. }  

性能分析: 哇靠!时间复杂度为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。

Cpp代码  收藏代码
  1. //解决方案2  
  2. void solution_2( T BigArr[], T ResArr[] ){  
  3.        //取最前面的一万个  
  4.        memcpy( ResArr, BigArr, sizeof(T) * RES_ARR_SIZE );  
  5.        //标记是否发生过交换  
  6.        bool bExchanged = true;  
  7.        //遍历后续的元素  
  8.        forint i = RES_ARR_SIZE; i < BIG_ARR_SIZE; ++i ){  
  9.               int idx;  
  10.               //如果上一轮发生过交换  
  11.               if( bExchanged ){  
  12.                      //找出ResArr中最小的元素  
  13.                      int j;  
  14.                      for( idx = 0, j = 1; j < RES_ARR_SIZE; ++j ){  
  15.                             if( ResArr[idx] > ResArr[j] )  
  16.                                    idx = j;  
  17.                      }  
  18.               }  
  19.               //这个后续元素比ResArr中最小的元素大,则替换。  
  20.               if( BigArr[i] > ResArr[idx] ){  
  21.                      bExchanged = true;  
  22.                      ResArr[idx] = BigArr[i];  
  23.               }else  
  24.                      bExchanged = false;  
  25.        }  
  26. }  

 

性能分析: 最坏的时间复杂度为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的最小值即可。与使用堆的过程如出一辙。

posted @ 2013-04-19 23:36  c_cloud  阅读(887)  评论(0编辑  收藏  举报