找最小的K個數

來源:互聯網
上載者:User

今天在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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.