Java最小堆解決TopK問題

www.toutiao.im

其實我們與大數據並不遙遠,比如要從海量數據中按大小或頻率挑出top k,假定機器是多核的內存有限的,我們採用多線程分塊處理數據,最後合併處理。那麼,處理每一塊數據的top k(i)可以採用哪些算法呢?

 

TopK問題是指從大量數據(源數據)中獲取最大(或最小)的K個數據。

TopK問題是個很常見的問題:例如學校要從全校學生中找到成績最高的500名學生,再例如某搜索引擎要統計每天的100條搜索次數最多的關鍵詞。

 

對於這個問題,解決方法有很多:

 

方法一:對源數據中所有數據進行排序,取出前K個數據,就是TopK。

但是當數據量很大時,只需要k個最大的數,整體排序很耗時,效率不高。

方法二:維護一個K長度的數組a[],先讀取源數據中的前K個放入數組,對該數組進行升序排序,再依次讀取源數據第K個以後的數據,和數組中最小的元素(a[0])比較,如果小於a[0]直接pass,大於的話,就丟棄最小的元素a[0],利用二分法找到其位置,然後該位置前的數組元素整體向前移位,直到源數據讀取結束。

這比方法一效率會有很大的提高,但是當K的值較大的時候,長度爲K的數據整體移位,也是非常耗時的。

 

對於這種問題,效率比較高的解決方法是使用最小堆

 

最小堆(小根堆)是一種數據結構,它首先是一顆完全二叉樹,並且,它所有父節點的值小於或等於兩個子節點的值

最小堆的存儲結構(物理結構)實際上是一個數組。如下圖:

 

 

堆有幾個重要操作:

BuildHeap:將普通數組轉換成堆,轉換完成後,數組就符合堆的特性:所有父節點的值小於或等於兩個子節點的值。

Heapify(int i):當元素i的左右子樹都是小根堆時,通過Heapify讓i元素下降到適當的位置,以符合堆的性質。

 

回到上面的取TopK問題上,用最小堆的解決方法就是:先取源數據中的K個元素放到一個長度爲K的數組中去,再把數組轉換成最小堆。再依次取源數據中的K個之後的數據和堆的根節點(數組的第一個元素)比較,根據最小堆的性質,根節點一定是堆中最小的元素,如果小於它,則直接pass,大於的話,就替換掉跟元素,並對根元素進行Heapify,直到源數據遍歷結束。

 

 

最小堆的實現:

[java]  view plain  copy
  1. public class MinHeap  
  2. {  
  3.     // 堆的存儲結構 - 數組  
  4.     private int[] data;  
  5.       
  6.     // 將一個數組傳入構造方法,並轉換成一個小根堆  
  7.     public MinHeap(int[] data)  
  8.     {  
  9.         this.data = data;  
  10.         buildHeap();  
  11.     }  
  12.       
  13.     // 將數組轉換成最小堆  
  14.     private void buildHeap()  
  15.     {  
  16.         // 完全二叉樹只有數組下標小於或等於 (data.length) / 2 - 1 的元素有孩子結點,遍歷這些結點。  
  17.         // *比如上面的圖中,數組有10個元素, (data.length) / 2 - 1的值爲4,a[4]有孩子結點,但a[5]沒有*  
  18.         for (int i = (data.length) / 2 - 1; i >= 0; i--)   
  19.         {  
  20.             // 對有孩子結點的元素heapify  
  21.             heapify(i);  
  22.         }  
  23.     }  
  24.       
  25.     private void heapify(int i)  
  26.     {  
  27.         // 獲取左右結點的數組下標  
  28.         int l = left(i);    
  29.         int r = right(i);  
  30.           
  31.         // 這是一個臨時變量,表示 跟結點、左結點、右結點中最小的值的結點的下標  
  32.         int smallest = i;  
  33.           
  34.         // 存在左結點,且左結點的值小於根結點的值  
  35.         if (l < data.length && data[l] < data[i])    
  36.             smallest = l;    
  37.           
  38.         // 存在右結點,且右結點的值小於以上比較的較小值  
  39.         if (r < data.length && data[r] < data[smallest])    
  40.             smallest = r;    
  41.           
  42.         // 左右結點的值都大於根節點,直接return,不做任何操作  
  43.         if (i == smallest)    
  44.             return;    
  45.           
  46.         // 交換根節點和左右結點中最小的那個值,把根節點的值替換下去  
  47.         swap(i, smallest);  
  48.           
  49.         // 由於替換後左右子樹會被影響,所以要對受影響的子樹再進行heapify  
  50.         heapify(smallest);  
  51.     }  
  52.       
  53.     // 獲取右結點的數組下標  
  54.     private int right(int i)  
  55.     {    
  56.         return (i + 1) << 1;    
  57.     }     
  58.   
  59.     // 獲取左結點的數組下標  
  60.     private int left(int i)   
  61.     {    
  62.         return ((i + 1) << 1) - 1;    
  63.     }  
  64.       
  65.     // 交換元素位置  
  66.     private void swap(int i, int j)   
  67.     {    
  68.         int tmp = data[i];    
  69.         data[i] = data[j];    
  70.         data[j] = tmp;    
  71.     }  
  72.       
  73.     // 獲取對中的最小的元素,根元素  
  74.     public int getRoot()  
  75.     {  
  76.             return data[0];  
  77.     }  
  78.   
  79.     // 替換根元素,並重新heapify  
  80.     public void setRoot(int root)  
  81.     {  
  82.         data[0] = root;  
  83.         heapify(0);  
  84.     }  
  85. }  

 

利用最小堆獲取TopK:

[java]  view plain  copy
  1. public class TopK  
  2. {  
  3.     public static void main(String[] args)  
  4.     {  
  5.         // 源數據  
  6.         int[] data = {56,275,12,6,45,478,41,1236,456,12,546,45};  
  7.           
  8. // 獲取Top5  
  9.         int[] top5 = topK(data, 5);  
  10.           
  11.         for(int i=0;i<5;i++)  
  12.         {  
  13.             System.out.println(top5[i]);  
  14.         }  
  15.     }  
  16.       
  17.     // 從data數組中獲取最大的k個數  
  18.     private static int[] topK(int[] data,int k)  
  19.     {  
  20.         // 先取K個元素放入一個數組topk中  
  21.         int[] topk = new int[k];   
  22.         for(int i = 0;i< k;i++)  
  23.         {  
  24.             topk[i] = data[i];  
  25.         }  
  26.           
  27.         // 轉換成最小堆  
  28.         MinHeap heap = new MinHeap(topk);  
  29.           
  30.         // 從k開始,遍歷data  
  31.         for(int i= k;i<data.length;i++)  
  32.         {  
  33.             int root = heap.getRoot();  
  34.               
  35.             // 當數據大於堆中最小的數(根節點)時,替換堆中的根節點,再轉換成堆  
  36.             if(data[i] > root)  
  37.             {  
  38.                 heap.setRoot(data[i]);  
  39.             }  
  40.         }  
  41.           
  42.         return topk;  
  43. }  
  44. }  

介紹完了最小堆,顧名思義,相應的還有最大堆。

www.toutiao.im

 

系統可用內存10M,從一個2G的文本文件裏統計出現次數排名前10的單詞,用MapReduce再合適不過,分而治之。

 

轉載於:https://my.oschina.net/javahongxi/blog/1523726