Java最小堆解決TopK問題
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,直到源資料遍歷結束。
最小堆的實現:
- publicclass MinHeap
- {
- // 堆的儲存結構 - 陣列
- privateint[] data;
- // 將一個數組傳入構造方法,並轉換成一個小根堆
-
public
- {
- this.data = data;
- buildHeap();
- }
- // 將陣列轉換成最小堆
- privatevoid buildHeap()
- {
- // 完全二叉樹只有陣列下標小於或等於 (data.length) / 2 - 1 的元素有孩子結點,遍歷這些結點。
- // *比如上面的圖中,陣列有10個元素, (data.length) / 2 - 1的值為4,a[4]有孩子結點,但a[5]沒有*
- for (int i = (data.length) / 2 - 1; i >= 0; i--)
- {
- // 對有孩子結點的元素heapify
- heapify(i);
- }
- }
- privatevoid heapify(int i)
- {
- // 獲取左右結點的陣列下標
- int l = left(i);
- int r = right(i);
- // 這是一個臨時變數,表示 跟結點、左結點、右結點中最小的值的結點的下標
- int smallest = i;
- // 存在左結點,且左結點的值小於根結點的值
- if (l < data.length && data[l] < data[i])
- smallest = l;
- // 存在右結點,且右結點的值小於以上比較的較小值
- if (r < data.length && data[r] < data[smallest])
- smallest = r;
- // 左右結點的值都大於根節點,直接return,不做任何操作
- if (i == smallest)
- return;
- // 交換根節點和左右結點中最小的那個值,把根節點的值替換下去
- swap(i, smallest);
- // 由於替換後左右子樹會被影響,所以要對受影響的子樹再進行heapify
- heapify(smallest);
- }
- // 獲取右結點的陣列下標
- privateint right(int i)
- {
- return (i + 1) << 1;
- }
- // 獲取左結點的陣列下標
- privateint left(int i)
- {
- return ((i + 1) << 1) - 1;
- }
- // 交換元素位置
- privatevoid swap(int i, int j)
- {
- int tmp = data[i];
- data[i] = data[j];
- data[j] = tmp;
- }
- // 獲取對中的最小的元素,根元素
- publicint getRoot()
- {
- return data[0];
- }
- // 替換根元素,並重新heapify
- publicvoid setRoot(int root)
- {
- data[0] = root;
- heapify(0);
- }
- }
利用最小堆獲取TopK:
- publicclass TopK
- {
- publicstaticvoid main(String[] args)
- {
- // 源資料
- int[] data = {56,275,12,6,45,478,41,1236,456,12,546,45};
- // 獲取Top5
- int[] top5 = topK(data, 5);
- for(int i=0;i<5;i++)
- {
- System.out.println(top5[i]);
- }
- }
- // 從data陣列中獲取最大的k個數
- privatestaticint[] topK(int[] data,int k)
- {
- // 先取K個元素放入一個數組topk中
- int[] topk = newint[k];
- for(int i = 0;i< k;i++)
- {
- topk[i] = data[i];