數據規模分析
不考慮操作系統的區別,通常將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的最小值即可。與使用堆的過程如出一轍