找最小的K個數

今天在CSDN無意中看到July一篇號稱《當今世界最爲經典的十大算法》的博文,感覺這文章名字挺霸氣,於是進去瞅了一眼。看到其中有一個叫做BFPRT的算法,據說可以最壞情況下也能以O(N)複雜度找到數組中的第K大元素。博文裏有鏈接到詳細解釋這個算法的另外一篇博文,於是又點進去,準備看看這算法是如何神奇,居然可以如此高效!

文章是以這樣一個問題開始的:如何在一堆數據中找出最小的K個數。

隨便想了一下,想了幾種方法:

1. 先對數據排序,然後取出前K個數。排序算法很多,什麼插入排序、快速排序等等,不過都不可能最壞也能到達O(N);

2. 開一個 |K| 大小的數組,先從這堆數據中裝入前K個數,找出這K個數中的最大數Max(K),然後從第K+1個數開始向後找,如果有小於這個Max(K)的,則替換掉這個數,然後從這K個數中重新找出最大的Max(K)。這樣一直向後掃描,得到結果。這個算法的複雜度最壞是O(Kn),也不行;

3. 堆,腦子裏閃過這麼個想法,只能算是靈感之類的東西,不過沒細想。

因爲急切想看看這個所謂的BFPRT算法到底是怎麼回事,所以只是稍微思考了一下,沒怎麼細想。

博文也列出了幾個可能解決這個問題的解法:

1. 跟我想的一樣,排序,取數,最笨的方法,也是最容易想到的;

2. 也跟我想的一樣,同上面的2(看來我還不是最笨的);

3. 比較笨的方法,掃描數據堆K遍,每遍找出最小的那個數,複雜度爲O(Kn);

4. 果然用到堆(可惜我沒細想)!想法跟2類似,先用數據中的前K個數建一個最大堆,建堆複雜度是O(K),然後從第K+1個數開始向後掃描,遇到小於堆頂元素時替換掉堆頂元素,更新堆,這個操作的複雜度是O(logK)。所以總的時間是O(K+(n-K)*logK)=O(n*logK),比方法2的O(nK)稍微好一點。

這種方法有個好處,就是當數據量很大時,如果內存放不下所有的數據,用這方法可以解決這個問題。先讀出一部分數據,建堆,處理完這部分數據,再讀出一部分數據,如此循環下去,直達數據處理完爲止。

5. 也是用堆,不過是對整個數據建一個最小堆(O(n)),然後取出堆頂元素,每取完一次更新一次堆(O(logn)),取K次,所以總的複雜度是O(n+K*logn);

可以證明O(n+K*logn) < O(n*logK),即建立n個元素是最小堆然後取前K個堆頂元素的方法比建立一個K個元素的最大堆然後比較所有數據得到最小的K個數的方法在時間複雜度上稍微優越一點,但兩者實際上是一個數量級,在那篇博文裏面,作者特意寫了實現了這兩種方法去處理一組大數據,結果表明兩種方法的時間實際上相差不多。

但在空間上,最大堆只需要O(K)的空間複雜度,而最小堆需要O(n),所以綜合來講,最大堆解決這種方法比最小堆有優勢。

算法改進:每次取走堆頂元素更新堆時,正常是把堆中最後一個元素放到堆頂(暫且稱爲 !Top),然後調整堆把 !Top下調到他應該在的位置。改進後, !Top不用下調到他原所應該在的位置,而是下調頂多K次就可以了。具體如下:

建立n的最小堆之後,取走堆頂元素(第一個數),然後將最後的數 !Top調到堆頂,把 !Top下調至多K-1層形成新的堆;接着取走堆頂元素(第二個數),同樣,更新堆的時候 !Top下調至多K-2層...直到取走第K個數時,不再更新堆(此時的堆已經不是最小堆),算法結束,已經取得最小的K個數,最後的“堆”是不是堆已經跟我沒關係了。

改進後的複雜度:建堆O(n),更新堆O(K),K次更新爲O(K*K)=O(K^2),所以總的複雜度是O(n+K^2),比改進前的O(n+K*logn)要好。

6. 用快速排序的思想,先選取一個數作爲基準比較數(作者稱爲“樞紐元”,即pivot),用快排方法把數據分爲兩部分Sa和Sb。

如果K< |Sa|( |Sa|表示Sa的大小),則對Sa部分用同樣的方法繼續操作;

如果K= |Sa|,則Sa是所求的數;

如果K= |Sa| + 1,則Sa和這個pivot一起構成所求解;

如果K> |Sa| + 1,則對Sb部分用同樣的方法查找最小的(K- |Sa|-1)個數(其中Sa和pivot已經是解的一部分了)。

與快排不同,快排每次都要對劃分後的兩部分數據都繼續進行同樣的快排操作,快速選擇(暫時這麼稱呼這種算法吧)不同,只對其中一部分進行操作即可。

BFPRT算法就是在這個方法的基礎上進行改進的。BFPRT算法主要改進是在選取pivot上面,一般快排是在數據堆取第一個或最後一個數最爲pivot,而BFPRT算法採用“五分化中位數的中位數”方法取得這個pivot,從而使算法複雜度降低到O(N),具體方法如下:

5個爲一組對數據進行劃分,最後一組數據的個數爲n%5,然後對每組數據用插入排序方法選出中位數,對選出的中位數用同樣的方法繼續選,最後選出這些數的中位數作爲pivot,即可達到O(N)的效率。

算法的具體證明我沒仔細看。那篇博文實在太長,估計是續寫了好多次,修改了n遍,感覺組織有點亂,看到頭有點暈,再找時間仔細看這算法的證明。具體可以參考:

Mark Allen Weiss的數據結構與算法分析--c語言描述,第10章,第10.2.3節

算法導論,第9.2、9.3節

編程之美第一版,第141頁,第2.5節 尋找最大的k個數

M. Blum, R.W. Floyd, V. Pratt, R. Rivest and R. Tarjan, "Time bounds for selection"

編程珠璣II 第15章第15.2節程序


總結:如果當時細想的話說不定也能多想幾個方法;有了解法之後,多想想這樣的算法是不是已經最優,還能不能再優化,比如一開始用排序,排序需要浪費時間,是不是可以不用排序的方法,不用排序方法裏面可能想到堆,堆是不是每次都必須調整到“完全正確的堆”,也可能用快排,快排是不是每次都排兩部分劃分的數據,等等;多想想把學過的數據結構靈活應用,比如這裏面用到的堆和快排,以前是用於數據排序,現在用來數據選擇,blahblah...總結完畢。


本文參考:《程序員編程藝術:第三章、尋找最小的k個數》http://blog.csdn.net/v_JULY_v/article/details/6370650

發佈了30 篇原創文章 · 獲贊 66 · 訪問量 65萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章