漫談topK 問題

轉自:csdn論壇
首先給出topK的經典學習資料,糧食如下:
v_july_v的//不僅僅有算法,也有實踐

《程序員編程藝術:第三章續、Top K算法問題的實現》
http://blog.csdn.net/v_july_v/article/details/6403777
《程序員面試題狂想曲:第三章、尋找最小的k個數》
http://blog.csdn.net/v_JULY_v/archive/2011/04/28/6370650.aspx

經典的《編程之美》 //下文有時簡稱bop



這裏總結的問題是:
topK這裏以求N個數種求 前k大爲 例子,求前k小可以類推。
這裏排序的內容就是 整數,
如果排序內容不僅僅有整數,可以參考《編程之美》解法2和解法5.


下面是我個人的讀後感,
首先說,我認爲topk,其實是排序算法應用的集大成者。
其實更精確說是部分的堆排序和快速劃分(欲知詳情,請看下文)
簡言之,思考topk的過程,其實是對各種算法進行pk的過程,也是認清算法優劣的過程。



下面我們來詳細思考這個問題。


1按照排序算法複雜度逐漸遞減的觀點看

  a. 一般,來個全排序(不妨使用快速排序(基於比較的最快的之一)),
  然後輸出前k大的數據。


表格如下:
  平均時間複雜度 最壞時間複雜度 空間複雜度//插入不了表格
  N*LogN + K N*N+K N



這是一個初步的,當然我們大都不會採用的算法哈。
尤其海量數據時,空間佔用爲o(n),直接會不予考慮。
我認爲這個算法最大的弱點 就是 做了過多的排序,其實只需部分排序。

---------------------------------------------------

  b. k趟輸出最大

每次取出集合中的最大,然後集合中排出最大,如此操作k次。

  複雜度表格如下:
  平均時間複雜度 最壞時間複雜度 空間複雜度
  N*K N*K N  




原本說要寫 k趟冒泡排序,但是我想在冒泡中有不少swap操作,不如直接比較操作好。

關於如何實現,我會嘗試如下:

標示已經選擇的元素方法可以考慮:

a)使用標誌數組標示已經找到的最大值,每次輸出最大後,爲標誌數組置位。(空間花銷大)
b)或者找出最大值後,直接輸出,然後將該位置置爲特殊數字或者字符。(排序的內容受限)
c)或者用stl中的stack存儲,用max尋找最值,後輸出max,用popup操作去除找到的值。(綜合排序和標示)

這種算法比第一種全排序好點,但是依然沒有優化,排序策略太簡單。前後兩次兩次
  求極值沒有關聯(除了排序的元素少了一個而已)。
一個優秀的排序,應該做到前後的排序做到銜接,比如:快速排序就是每趟
  將數據分成2個集合,而堆排序是每次調整都做到局部有序以至於每次調整操作放得複雜度只有logN.

下面我將會介紹基於堆排序和 快速排序的topK的解法。


------------------end b-------------------------------


  c. 堆排序的變形:部分堆排序
千呼萬喚始出來,其實如果要一句話回答topk的解法,很多人會條件反射的說 不就是個堆排序。
當然!
使用堆排序求topk,方法詳細見bop書中的解法4。


堆排序的核心操作是建立初始堆和更新堆。 或者換個角度說是函數siftdown 和函數siftup(來自於編程珠璣函數命名) ,如果再抓重點,我會說是siftdown,原因是: siftup功能其實就是siftdown。值得注意的事siftdown一般實踐爲遞歸函數,這裏可以換成非遞歸以提升性能。

需要注意的是這裏的堆排序並非完全的堆排序。
第一次我接觸到這個方法時,我始終想不通 要輸出最大值,爲什麼要使用最小堆。
後來才明白這個問題與傳統的堆排序不一樣

a)傳統的堆是要把所有的元素放入堆,
而這裏是只放入k個,而不是N個。

b)更新堆的操作中,
傳統的堆排序有:
i)傳統的堆排序是取自於堆末尾,堆尾巴與堆頭交換後開始調整。
Ii)每次調整後堆尾巴往前面移動一位,堆的大小減一。
Iii)終止條件是最後移到堆頭部爲止。

topK(max k)的堆排序,其實簡言之是部分的堆排序。
i)每次與堆頭部交換的數據來自於外部,不來自於堆內部。並且交換並不是必須的,只有大於堆頭部的纔會被交換
Ii)每次調整堆後,堆的大小不變,
Iii)終止條件是處理了剩下的N-k的數據。

  最後列出複雜度表格如下:
  平均時間複雜度 最壞時間複雜度 空間複雜度
  N*logK N*logK k  

  ps:還有一個使用大頂堆來排 前k大問題.,複雜度盡然達到了O(k*k)
但是作者說雖然複雜度劇減,但是常數因子不可忽視,還是推薦使用常用的小頂堆,具體情況請看v_july_v的博客。

-----------------------------end c----------------------




  d.快速排序的變形:快速劃分
快速排序這個算法再經典不過了,經典的問題可以反覆品味,關於這個專題,v_july_v就寫了至少2個博客,

寫一個快速排序容易,但是寫一個高效的快速排序可不是一件容易的事情啊。關於快速排序的優化,後面我會寫個專題來討論一下(待補充)。

首先說,我們不會照搬快速排序,
如果只是使用快速排序,可以看第一種情況,時間複雜度和空間複雜度都很高。

不妨我們來看看快速排序的實現過程,
先劃分區間,然後分兩個區間再次調用快速排序。

那麼快速排序又是怎麼實現的?其實代碼會說話,詳細的請看:
《程序員編程藝術:第三章續、Top K算法問題的實現》
http://blog.csdn.net/v_july_v/article/details/6403777

這裏貼出 以使用隨機劃分算法的快速排序重要代碼,個人覺得覺得比bop書 上清晰。

int random_select(int array[], int left, int right, int k)      
{  
    // 處理異常情況  
    if (k < 1 || k > (right - left + 1))  
        return -1;   
    // 主元在數組中的位置  
    int pos = random_partition(array, left, right);      
      
    /* 對三種情況進行處理:(m = i - left + 1)
    1、如果m=k,即返回的主元即爲我們要找的第k小的元素,那麼直接返回主元
  array即可;
    2、如果m>k,那麼接下來要到低區間array[left....pos-1]中尋找,丟掉高區間;
    3、如果m<k,那麼接下來要到高區間array[pos+1...right]中尋找,丟掉低區間。
    */  
    int m = pos - left + 1;  
    if(m == k)  
        return array[pos];                              
    else if (m > k)         
        return random_select(array, left, pos - 1, k);  
    else   
        return random_select(array, pos + 1, right, k - m);  
}  



這裏我想說的快速排序和快速分劃有哪些不同:
a)遞歸出口不一樣。(這裏討論的正常出口,等於 )
前者是區間長度爲1返回,後者是區間長度爲k時。
b)主函數中,快速排序中必須對兩個區間進行遞歸。
而快速劃分只對某一種情況進行遞歸。
  
那麼快排和快速分化有那些相同點:
a)可以這麼說,快速排序和快速排序的分劃函數是一樣的。
b)關於異常區間的處理一樣:區間長度小於0,二者遞歸都結束。區間長度小於k值也退出。



  最後列出複雜度表格如下:
  劃分方法 平均時間複雜度 最壞時間複雜度 空間複雜度
  直接使用左(右)邊界爲軸 N*logN N*N N
  三元取中法 N N N
  五分化中項的中法 N N N
  隨機選取樞紐元法 N N N





個人認爲:平時使用中 使用三元取中法既可以了,使用五分化中項樞元法(詳細請看 算法導論 快速排序章節)和隨機選取樞紐元法較之比較複雜。

這裏解釋一下什麼是快速劃分的最壞情況,其實這跟快速排序最壞情況一樣,元素排列完全逆序下。

最後小結下,這裏快速分劃直接可以達到了線性複雜度!!

那麼還有那些算法可以達到線性複雜度。
下面一一理出。

-----------------------------end d----------------------


  e.線性算法系列
除開上面使用的快速分划算法,還有計數排序,位圖法。

a)計數排序。
核心思想就是被排序值做數組下標,數組裏的值裝入對應數的頻率數,最後按照頻率輸出前k個。
b)位圖法。
如果所有數字最多出現一次,使用位圖法最好不過。這是最節約空間的一種。
兩者算法都對排序的數據集有要求,所有書都是正整數,並且取值範圍不要太大。

-----------------------------end e----------------------

  f.其他
以上都是以排序的角度看待問題,
  在bop書上,可以看到還有使用查找算法來解決。

二分查找的變形(複雜度n*logk)
主要思想是:找出前k大元素 等價於 找出第k大元素 。


2.按照k和n的相對數量級來選擇算法(待完善)

這個觀點來源於《編程之美》該問題最後的小節提出的見解。
我試着寫出我的見解。

其實除了k.n的相對大小,還有數據集組成情況。
a)只有正整數 ; 只有整數;含浮點數;
b)數據集中與否
c)數據有重複嗎

當前考慮一般情況,所以兩種特殊的線性算法(計數排序和位圖)暫不考慮

如果k=100,算法選擇的表格如下:
  
  n(數據集合大小) 1000 10000 k 10 000 000 k(換算約10g)
  內存不受限 快速劃分 快速劃分 快速劃分  
  內存受限(2G爲例)快速劃分 快速劃分 堆排序+歸併排序


一句話,大多數情況,除了內部不足,我都會考慮 快速劃分的。
上述表格是我今天總結的,大家覺得有什麼不妥的,儘管提意見。



3.小結:
  topk問題說大也不大,說笑也不小,從上午開始寫,各種彙總資料,到現在過去5個小時,寫之前以爲自己懂了,寫之後發現依然還有問題的缺口,比如算法的選擇策略上如何更爲完整的總結,希望與網友共勉。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章