1. 程式人生 > >典型的Top K演算法 _找出一個數組裡面前K個最大數_找出1億個浮點數中最大的10000個_一個文字檔案,找出前10個經常出現的詞,但這次檔案比較長,說是上億行或十億行,總之無法一次讀入記憶體.

典型的Top K演算法 _找出一個數組裡面前K個最大數_找出1億個浮點數中最大的10000個_一個文字檔案,找出前10個經常出現的詞,但這次檔案比較長,說是上億行或十億行,總之無法一次讀入記憶體.

        搜尋引擎會通過日誌檔案把使用者每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255位元組。
        假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的使用者越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的記憶體不能超過1G

必備知識:
什麼是雜湊表?

        雜湊表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的資料結構。

        也就是說,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度。這個對映函式叫做雜湊函式,存放記錄的陣列叫做散列表。

雜湊表的做法其實很簡單,就是把Key通過一個固定的演算法函式既所謂的雜湊函式轉換成一個整型數字,然後就將該數字對陣列長度進行取餘,取餘結果就當作陣列的下標,將value儲存在以該數字為下標的陣列空間裡。
       而當使用雜湊表進行查詢的時候,就是再次使用雜湊函式將key轉換為對應的陣列下標,並定位到該空間獲取value,如此一來,就可以充分利用到陣列的定位效能進行資料定位。

解法一:
問題解析:

要統計最熱門查詢,首先就是要統計每個Query出現的次數,然後根據統計結果,找出Top 10。所以我們可以基於這個思路分兩步來設計該演算法。

即,此問題的解決分為以下倆個步驟:

        Query統計有以下倆個方法,可供選擇:
        1、直接排序法   (經常在日誌檔案中統計時,使用cat file|format key|sort | uniq -c | sort -nr | head -n 10,就是這種方法)

        首先我們最先想到的的演算法就是排序了,首先對這個日誌裡面的所有Query都進行排序,然後再遍歷排好序的Query,統計每個Query出現的次數了。

但是題目中有明確要求,那就是記憶體不能超過1G,一千萬條記錄,每條記錄是255Byte,很顯然要佔據2.375G記憶體,這個條件就不滿足要求了。

讓我們回憶一下資料結構課程上的內容,當資料量比較大而且記憶體無法裝下的時候,我們可以採用外排序的方法來進行排序,這裡我們可以採用歸併排序,因為歸併排序有一個比較好的時間複雜度O(NlogN)。

排完序之後我們再對已經有序的Query檔案進行遍歷,統計每個Query出現的次數,再次寫入檔案中。

綜合分析一下。

演算法一:普通排序             (我們只用找出top10,所以全部排序有冗餘)

如果選擇像快速排序,堆排序這樣全排序的時間複雜度是O(NlogN),而遍歷的時間複雜度是O(N),因此該演算法的總體時間複雜度就是O(N+NlogN)=O(NlogN)。

演算法二:部分排序 

上面的演算法對整個陣列都進行了排序,而原題目只要求最大的K個元素,並不需要前K個數有序,也不需要後N-K個元素有序。如何避免做後N-K個數的排序呢?我們選擇部分排序演算法。像:選擇排序,交換排序找出top k個元素的時間複雜度為O(NK)而遍歷的時間複雜度是O(N),因此該演算法的總體時間複雜度就是O(N+NK)=O(NK)。

解法二:

       2、Hash Table法                (這種方法統計字串出現的次數非常好)
       在第1個方法中,我們採用了排序的辦法來統計每個Query出現的次數,時間複雜度是N*logN,那麼能不能有更好的方法來儲存,而時間複雜度更低呢?

       題目中說明了,雖然有一千萬個Query,但是由於重複度比較高,因此事實上只有300萬的Query,每個Query 255Byte,因此我們可以考慮把他們都放進記憶體中去,而現在只是需要一個合適的資料結構,在這裡,Hash Table絕對是我們優先的選擇,因為Hash Table的查詢速度非常的快,幾乎是O(1)的時間複雜度。

       那麼,我們的演算法就有了:

維護一個Key為Query字串,Value為該Query出現次數的HashTable,每次讀取一個Query,如果該字串不在Table中,那麼加入該字串,並且將Value值設為1;如果該字串在Table中,那麼將該字串的計數加一即可。最終我們在O(N)的時間複雜度內完成了對該海量資料的處理。

                本方法相比演算法1:在時間複雜度上提高了一個數量級,為O(N),但不僅僅是時間複雜度上的優化,該方法只需要IO資料檔案一次,而演算法1的IO次數較多的,因此該演算法2比演算法1在工程上有更好的可操作性。

     演算法一:普通排序             (我們只用找出top10,所以全部排序有冗餘)
     我想對於排序演算法大家都已經不陌生了,這裡不在贅述,我們要注意的是排序演算法的時間複雜度是NlogN,在本題目中,三百萬條記錄,用1G記憶體是可以存下的。

     演算法二:部分排序         
     題目要求是求出Top 10,因此我們沒有必要對所有的Query都進行排序,我們只需要維護一個10個大小的陣列,初始化放入10個Query,按照每個Query的統計次數由大到小排序,然後遍歷這300萬條記錄,每讀一條記錄就和陣列最後一個Query對比,如果小於這個Query,那麼繼續遍歷,否則,將陣列中最後一條資料淘汰(還是要放在合適的位置,保持有序。),加入當前的Query,對陣列的十個資料排序。最後當所有的資料都遍歷完畢之後,那麼這個陣列中的10個Query便是我們要找的Top10了。

      不難分析出,這樣,演算法的最壞時間複雜度是N*K, 其中K是指top多少。(只對k個元素排序,可選擇部分排序演算法。用選擇排序,或者氣泡排序,時間複雜度也是O(N*K))

      演算法三:堆
       在演算法二中,我們已經將時間複雜度由NlogN優化到N*K,不得不說這是一個比較大的改進了,可是有沒有更好的辦法呢

       分析一下,在演算法二中,每次比較完成之後,需要的操作複雜度都是K,因為要把元素插入到一個線性表之中,而且採用的是順序比較。這裡我們注意一下,該陣列是有序的,一次我們每次查詢的時候可以採用二分的方法查詢,這樣操作的複雜度就降到了logK,可是,隨之而來的問題就是資料移動,因為移動資料次數增多了。不過,這個演算法還是比演算法二有了改進。

       基於以上的分析,我們想想,有沒有一種既能快速查詢,又能快速移動元素的資料結構呢?

       回答是肯定的,那就是堆。
       藉助堆結構,我們可以在log量級的時間內查詢和調整/移動。因此到這裡,我們的演算法可以改進為這樣,維護一個K(該題目中是10)大小的小頂堆,然後遍歷300萬的Query,分別和根元素進行比較。

思想與上述演算法二一致,只是在演算法三,我們採用了最小堆這種資料結構代替陣列,把查詢目標元素的時間複雜度有O(K)降到了O(logK)。
       那麼這樣,採用堆資料結構,演算法三,最終的時間複雜度就降到了N*logK,和演算法二相比,又有了比較大的改進。

堆用一個數組h[ ]表示,它的父節點為h[i/2],兒子節點是h[2*i+1]和h[2*i+2].

程式碼如下:

if(x>h[0])//如果當前數比小頂堆頂部元素大 
{  
    h[0]=x;
p=0;
while(p<k)
{
q=2*p+1;//當前根節點的左子節點。 
if(q>=k)//沒有孩子節點 (孩子節點編號超過k) 
break;
if((q<k-1)&&(h[q+1]<h[q]))//如果當前根節點的右子節點比左子節點小 
  q=q+1;
   if(h[q]<h[p])//如何孩子節點中最小的一個比父節點小,進行堆調整。 
   {
   t=h[p];
   h[p]=h[q];
   h[q]=t;
   p=q;
   }
   else
   break;
}
}


總結:

至此,演算法就完全結束了,經過上述第一步、先用Hash表統計每個Query出現的次數,O(N);然後第二步、採用堆資料結構找出Top 10,N*O(logK)。所以,我們最終的時間複雜度是:O(N) + N'*O(logK)。(N為1000萬,N’為300萬)。

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

問題一:

        找出一個無序數組裡面前K個最大數 演算法思想1

對陣列進行降序全排序,然後返回前K個元素,即是需要的K個最大數。

       排序演算法的選擇有很多,考慮陣列的無序性,可以考慮選擇快速排序演算法,其平均時間複雜度為O(NLogN)。具體程式碼實現可以參見相關資料結構與演算法書籍。

演算法思想2(比較好):

         觀察第一種演算法,問題只需要找出一個數組裡面前K個最大數,而第一種演算法對陣列進行全排序,不單單找出了前K個最大數,更找出了前N(N為陣列大小)個最大數,顯然該演算法存在“冗餘”,因此基於這樣一個原因,提出了改進的演算法二。 

         首先建立一個臨時陣列,陣列大小為K,從N中讀取K個數,降序全排序(排序演算法可以自行選擇,考慮陣列的無序性,可以考慮選擇快速排序演算法),然後依讀入其餘N - K個數進來和第K名元素比較,大於第K名元素的值則插入到合適位置,陣列最後一個元素溢位,反之小於等於第K名元素的值不進行插入操作。只待迴圈完畢返回臨時陣列的K個元素,即是需要的K個最大數。同演算法一其平均時間複雜度為O(KLogK + (N - K))。具體程式碼實現可以自行完成。

原文: 問題二:        有1億個浮點數,請找出其中最大的10000個。        提示:假設每個浮點數佔4個位元組,1億個浮點數就要站到相當大的空間,因此不能一次將全部讀入記憶體進行排序。

       可以發現如果一次讀入那麼機器的記憶體肯定是受不了的,因此我們只有想其他方法解決,解決方式為了高效還是得符合一定的該概率解決,結果並不一定準確,但是應該可以作對大部分的資料。

演算法思想1、
       1、我們可以把1億個浮點數利用雜湊分為了1000個組
(將相同的數字雜湊到同一個陣列中)

       2、第一次在每個組中找出最大的1W個數,共有1000個;

       3、第二次查詢的時候就是100W個數中再找出最大的1W個數。
       PS:100W個數中再找出最大的1W個數用類似快排的思想搞定。
演算法思想2(比較好)、
      1、讀入的頭10000個數,直接建立二叉排序樹。O(1)

      2、對以後每個讀入的數,比較是否比前10000個數中最小的大。(N次比較)如果小的話接著讀下面的數。O(N)
      3、如果大,查詢二叉排序樹,找到應當插入的位置。
       4、刪除當前最小的結點。
       5、重複步驟2,直到10億個數全都讀完。
       6、按照中序遍歷輸出當前二叉排序樹中的所有10000個數字。
       基本上演算法的時間複雜度是O(N)次比較
       演算法的空間複雜度是10000(常數)

       基於上面的想法,可以用最小堆來實現,這樣沒加入一個比10000個樹中最小的數大時的複雜度為log10000.

相關類似問題:

1、一個文字檔案,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。

     方案1:這題是考慮時間效率。用trie樹(字首樹)統計每個詞出現的次數,時間複雜度是O(n*le)(le表示單詞的平準長度)。然後是找出出現最頻繁的前10個詞,可以用堆來實現,前面的題中已經講到了,時間複雜度是O(n*lg10)。所以總的時間複雜度,是O(n*le)與O(n*lg10)中較大的哪一個。

2、 一個文字檔案,找出前10個經常出現的詞,但這次檔案比較長,說是上億行或十億行,總之無法一次讀入記憶體,問最優解。

     方案1:首先根據用hash並求模,將檔案分解為多個小檔案,對於單個檔案利用上題的方法求出每個檔案件中10個最常出現的詞。然後再進行歸併處理,找出最終的10個最常出現的詞。

3、 100w個數中找出最大的100個數。

  • 方案1:採用區域性淘汰法。選取前100個元素,並排序,記為序列L。然後一次掃描剩餘的元素x,與排好序的100個元素中最小的元素比,如果比這個最小的要大,那麼把這個最小的元素刪除,並把x利用插入排序的思想,插入到序列L中。依次迴圈,知道掃描了所有的元素。複雜度為O(100w*100)。
  • 方案2:採用快速排序的思想,每次分割之後只考慮比軸大的一部分,知道比軸大的一部分在比100多的時候,採用傳統排序演算法排序,取前100個。複雜度為O(100w*100)。
  • 方案3:在前面的題中,我們已經提到了,用一個含100個元素的最小堆完成。複雜度為O(100w*lg100)。