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的最小值即可。與使用堆的過程如出一轍

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章