給定乙個長度為
n的序列,不妨設為
l1,l2,l3,….,ln
。這個序列可以是任意一種排列,可能的排列有
n!種,我們要找到最小的
k個數,即找到這樣的k個數
,並滿足
li(1)<=li(2)<=li(3)…<=li(k)
;且對任意的j:
k+1<=j<=n
,有li(k)<=li(j)
,。例如:有這樣乙個長度為
8的序列
,找出最小的
3個數,則結果為
或者,而不是最小的
k個排序碼
。我們只討論
n較大的情況,不妨設
n至少百萬數量級(
m),下面對
k進行逐一的分析:
首先,回顧一下幾大類排序
氣泡排序
選擇排序:
快速排序,快速排序也可以看作一種選擇排序。
插入排序:
歸併排序:
基數排序:
計數排序:
其中,適合
topk
問題的排序,只有氣泡排序,選擇排序和快速排序,其餘的排序都必須在排序最後一刻才能知道誰是最小的
k個元素。
如果k = 1
,此時很顯然只能在簡單選擇排序和氣泡排序中考慮,因為錦標賽排序和堆排序初始化的成本太大。而氣泡排序在最壞情況下需要有3(
n-1)次移動,即如果初始排列正好是降序的情況。而簡單選擇排序,只需要掃瞄一遍,無需移動資料。但氣泡排序有乙個其他排序都不具備的性質就是,如果初始排列恰好是有序的(公升序),則一趟冒泡就可以知道這個資訊。因此在
k > 1
時,可以考慮第一趟用冒泡的方法,既能判斷出初始序列是否有序,也能夠在o(
n)的時間內找到最小值,一舉兩得。
如果1 < k < c1(
某個小常數
c1),則繼續使用簡單選擇排序也是理想的,這個
c1的臨界點恰好是錦標賽排序和堆排序這種複雜排序初始化的時間開銷引起的,當越過了
c1的臨界點後,錦標賽排序和堆排序的優勢就發揮了出來。在這種情況下,簡單選擇排序,需要比較的次數為
n-1+n-2+…+n-c1
次。從記憶體的層次結構的角度看,複雜選擇排序
(非線性
)和簡單選擇排序(線性)相比,快取的命中率更低,換入換出的代價較大,且堆排序的初始化過程雖然複雜度也為o(
n),但在
n很大的情況下,最壞情況下,係數接近4。
如果c1 <= k < c2
,此時錦標賽排序會是更理想的選擇,和堆排序相比,錦標賽排序樹是乙個完全二叉樹(有些教材認為是滿二叉樹,這是不夠好的),需要
n-1個輔助空間,但錦標賽排序的初始化比較次數很少,只有
n-1次(沒有最好最壞之分)和堆排序的
4n(最壞情況下)
相比,在選出最小的元素後,選擇後續的元素,堆排序和錦標賽排序都需要調整,錦標賽從底向上調整,堆排序從上向下調整,但錦標賽排序每上公升一格只需要比較
1次,堆排序需要比較
2次(據稱採用加速堆的方法,可以把這個係數降到
1,但需要付出
lglgn
的代價,同時付出**的複雜性,本文不深入討論這一點)。錦標賽排序總需要從葉子到根,而堆排序可能不需要,比如
n個元素都相同的情況下,後者其他恰好符合堆性質而不需調整,或不需調整到葉子,總體情況看,錦標賽排序佔據初始化的優勢,在排序的早期應該能夠勝出堆排序。當然錦標賽排序和堆排序都有優化提高的空間,就優化後的比較本文不作**。
如果c2 <= k < n/2
,堆排序將會是更理想的選擇,堆排序是一種原地排序,輔助空間為o(
1),因此空間區域性性更好,特別是如果把堆看做乙個陣列,那麼隨著排序的進行,主要的計算都集中在陣列的一段,而且區域性性越來越好,因為陣列的尾部已經是排好序的,沒有訪問的必要了。為了找出最小的
k個數,堆排序將會使用小根堆,輸出時從陣列的尾部反序輸出。
如果n/2 < k
,此時可以考慮用快速排序和堆排序結合的方法。前幾趟用快速排序,快速壓縮問題空間。使得問題轉化為在
l長度序列的排序
+ m個序列中找
top-k』
個元素的問題或者在
l』長度序列中找
top-k
的問題。
舉個例子,例如
中找前5
個元素,則通過
4的劃分後得到
4 ,由於已知4是第
4大的元素,則只需要將前一段全部排序輸出,再輸出
4,在輸出後一段的第
5 – 4 = 1
個元素即可。如果是找前
2個元素,問題就歸結為在
中找最小的
2個元素,則問題將大大化簡。當k
接近n的時候,毫無疑問使用快排序應該是最理想的了。
最後,我們再討論一下,當
n足夠大,以至於不能使用內排的情況。
由於這時問題的複雜性主要取決於讀盤,因此我們希望的是找出最小的
k個數的代價是唯讀一遍磁碟,同時考慮排序碼還有其他衛星資料的情況。
在這種情況下,直接選擇排序,只有在
k = 1
時,才是最理想的。
當k > 1
時,選擇堆排序時很理想的,因為錦標賽排序的輔組空間不能接受。可以設定乙個大小為
k的最大堆,該元素是整個堆最大的,如果在掃瞄磁碟的過程中,有排序碼比這個更大則
pass
,如果更小,則把這個值淘汰掉,插入這個更小的值後,恢復成乙個最大堆。掃瞄完畢後留在堆中的
k個值,即為所求。
如果有其他衛星資料的情況下,堆的結點只需要增加相應的資料域或指標域即可。
但問題是,假定實際的問題是需要在
100億網頁
url中,找到
pv最大的
top100
時(我們討論的是最小,最大也可以用一樣的方法)。我們使用了堆的方法,找到了結果,但是,領導突然需要看
top 1000
的url
時,還得做個大小為
1000
的堆再跑一遍,如果
topk
中的某些條件發生變化,比如
url長度在
512以上的排除。。。可見,用堆的方法沒有儲存有價值的中間結果,這是很不理想的。
什麼才是理想的中間結果呢?我們考慮使用計數排序,例如這樣乙個整數序列
,我們可以申請從1到
8的8個計數器,組成乙個陣列,pennyliang_
counter=
,其中pennyliang_counter[0] = 1
,表示1
出現了1
次,penny_
counter[3] = 2
,表示4
出現了2
次。輸出時,按照計數器陣列,輸出即為有序序列,計數排序是線性的,而且計數器陣列是理想的排序中間結果。
對於top-k pv
的問題,申請乙個閾值以上的計數器,例如
1024
以上的pv
數值才能有可能申請計數器,對
10k以上的
pv數值,才申請儲存
url的空間,(後續的處理也有很多優化,本文不再展開)。掃瞄一趟磁碟,就可以生成這樣的計數器陣列,對於任意的處理要求,可以通過這個計數器陣列直接得到,最壞情況也可以通過這個計數器陣列加上再一次的磁碟掃瞄線性地得到。
昨夜,我一夜難眠,就在想怎麼把這個問題講清楚,臨到寫的時候,還是感到很多內容無法展開,同時感到自己對這個問題的理解還不能定量的分析,因此甚是遺憾
華為OJ2051 最小的K個數(Top K問題)
描述 輸入n個整數,輸出其中最小的k個。輸入 輸入 n 和 k 輸入乙個整數陣列 輸出 輸出乙個整數陣列 樣例輸入 5 2 1 3 5 7 2樣例輸出 1 2 對於 top k 問題有很多種解法。相信很多人會首先想到這種方法,先把陣列按公升序 降序進行排序,然後輸出 k 個最小 最大的數。由於我們只...
最小的K個數
問題描述 給定的n個整數,計算其中最小的k個數。最直觀的解法莫過於將n個數按公升序排列後輸出前k個。但是就效率來看,這種方法並不是最理想的。一種改進方法是借助快速排序中對陣列的劃分,以第k個元素對陣列進行劃分,使得比第k個數字小的數字都在其左邊,比其大的數字都在它的右邊。void swap int ...
最小的K個數
從 陣列中出現次數超過一半的數字 得到啟發,同樣可以基於partition函式來解決。一 o n 演算法 void getleastnumbers int input,int n,int output,int k else for int i 0 i k i output i input i 二 o...