Top k 算法

Top K 算法詳解
應用場景:

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

必備知識:
什麼是哈希表?

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

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

哈希表的做法其實很簡單,就是把Key通過一個固定的算法函數既所謂的哈希函數轉換成一個整型數字,然後就將該數字對數組長度進行取餘,取餘結果就當作數組的下標,將value存儲在以該數字爲下標的數組空間裏。
       而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換爲對應的數組下標,並定位到該空間獲取value,如此一來,就可以充分利用到數組的定位性能進行數據定位。
問題解析:

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

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

第一步:Query統計              (統計出每個Query出現的次數)
        Query統計有以下倆個方法,可供選擇:
        1、直接排序法                  (經常在日誌文件中統計時,使用cat file|format key|sort | uniq -c | sort -nr | head -n 10,就是這種方法)
        首先我們最先想到的的算法就是排序了,首先對這個日誌裏面的所有Query都進行排序,然後再遍歷排好序的Query,統計每個Query出現的次數了。

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

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

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

綜合分析一下,排序的時間複雜度是O(NlgN),而遍歷的時間複雜度是O(N),因此該算法的總體時間複雜度就是O(N+NlgN)=O(NlgN)。

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

       題目中說明了,雖然有一千萬個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在工程上有更好的可操作性。

     第二步:找出Top 10          (找出出現次數最多的10個)
     算法一:普通排序             (我們只用找出top10,所以全部排序有冗餘)
     我想對於排序算法大家都已經不陌生了,這裏不在贅述,我們要注意的是排序算法的時間複雜度是NlgN,在本題目中,三百萬條記錄,用1G內存是可以存下的。

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

      不難分析出,這樣,算法的最壞時間複雜度是N*K, 其中K是指top多少。

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

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

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

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

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

總結:

至此,算法就完全結束了,經過上述第一步、先用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)。
發佈了52 篇原創文章 · 獲贊 10 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章