資料結構演算法題/top K問題
問題描述:有N(N>>10000)個整數,求出其中的前K個最大的數。(稱作Top k或者Top 10)
問題分析:由於(1)輸入的大量資料;(2)只要前K個,對整個輸入資料的儲存和排序是相當的不可取的。
解決方案1:最小堆
可以利用資料結構的最小堆來處理該問題。最小堆如圖所示,對於每個非葉子節點的數值,一定不大於孩子節點的數值。這樣可用含有K個節點的最小堆來儲存K個目前的最大值(當然根節點是其中的最小數值)。
每次有資料輸入的時候可以先與根節點比較。若不大於根節點,則捨棄;否則用新數值替換根節點數值。並進行最小堆的調整。
解決方案:
(1)每次對前K個進行建最小堆,不是n個
(2)使用最小堆
原因是:首先選前k個構建最原始的最小堆。然後對於下一個輸入的資料,如果不大於根節點就捨棄,否則用新數值替換根節點數值,同時對最小對進行調整。(對於最小堆裡面的k個元素可以理解成目前最大的k個值。)
(3)時間複雜度是nlogk
由於儲存了K個數據,調整最小堆的時間複雜度為O(lnK),因此TOp K演算法(問題)時間複雜度為O(nlnK)
程式碼1使用java自帶的堆介面PriorityQueue
import java.util.PriorityQueue; /** * PriorityQueue實現了資料結構堆,通過指定comparator欄位來表示小頂堆或大頂堆 */ public class HeapTopK { public int findKthLargest(int[] nums, int k) { PriorityQueue<Integer> minQueue = new PriorityQueue<>(k); for (int num : nums) { if (minQueue.size() < k || num > minQueue.peek())// 堆還不滿k個的時候或者堆頂小於下一個要進來的資料時,將該資料插入最小堆 minQueue.offer(num); if (minQueue.size() > k) minQueue.poll(); } return minQueue.peek(); } public static void main(String[] args) { HeapTopK heapTopK = new HeapTopK(); int a[] = {1, 5, 6, 2, 4, 10}; System.out.println(heapTopK.findKthLargest(a, 3)); } }
解決方案2:quickselect(借鑑quicksort的思想,因為是top K所以也就是借鑑逆序排序的快排思想)時間複雜度nlogn。
本方法找出第k大的資料,在計算的過程中可以記錄top k的資料。quickselect和quicksort使用的相同的計算主元partition的方法。
選取一個基準元素pivot,將陣列切分(partition)為兩個子陣列,比pivot大的扔左子陣列,比pivot小的扔右子陣列,然後遞推地切分子陣列。Quick Select不同於Quick Sort的是其沒有對每個子陣列做切分,而是對目標子陣列做切分。其次,Quick Select與Quick Sort一樣,是一個不穩定的演算法;pivot選取直接影響了演算法的好壞,worst case下的時間複雜度達到了O(n2)。
Quick Select的目標是找出第k大元素,所以
- 若切分後的左子陣列的長度 > k,則第k大元素必出現在左子陣列中;
- 若切分後的左子陣列的長度 = k-1,則第k大元素為pivot;
- 若上述兩個條件均不滿足,則第k大元素必出現在右子陣列中
-
/** * Created by liyang54 on 2018/10/14. * 選擇topK需要逆序排序 * * 下面針對的是調整主元之後的陣列 * 若切分後的左子陣列的長度 > k,則第k大元素必出現在左子陣列中; * 若切分後的左子陣列的長度 = k-1,則第k大元素為pivot; * 若上述兩個條件均不滿足,則第k大元素必出現在右子陣列中。 */ public class QuickSelectTopK { // quick select to find the kth-largest element,返回第k個元素 // 遞迴的 public int quickSelect(int[] arr, int k, int left, int right) { if (left == right) return arr[right]; int index = partition(arr, left, right); if (index - left + 1 > k) return quickSelect(arr, k, left, index - 1); else if (index - left + 1 == k) return arr[index]; else return quickSelect(arr, k - index + left - 1, index + 1, right); //k - index + left - 1表示左邊(0,index)都不是了,已經去除了index-left+1個了, //只需要再剩下的k-(index-left+1)中找即可。 // (也就是在右邊的index+1到right這個區間的找的時候,不是原來的陣列A第K個了,是右邊新陣列的k - index + left - 1) } // 下面的計算主元的方法和快排的是一樣的,公用的 // 因為是topk,下面需要的是逆序的 // partition subarray a[left..right] so that a[left..j-1] >= a[j] >= a[j+1..right] // and return index j // 逆序排序的partition private int partition(int A[],int p,int r){ int x=A[r];//把每次陣列A的最後一個元素作為主元 int i=p-1;//開始的時候將i 移動到陣列的外面 for(int j=p;j<=r-1;j++){ if(A[j]>=x){ i++; swap(A, i,j);//p--i是小於等於x的,i+1--j-1是大於等於x的所以要交換下 } } swap(A, i+1, r);//把主元放在中間好區分兩邊的 return i+1;//返回主元的位置 } private void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } public static void main(String[] args) { QuickSelectTopK quickSelectTopK = new QuickSelectTopK(); int a[] = {1,36,2,5,7,4,9}; //下面輸出的是第三大的 System.out.println(quickSelectTopK.quickSelect(a, 3, 0, a.length-1)); } }