Top K問題的兩種解決思路


Top K問題在數據分析中非常普遍的一個問題(在面試中也經常被問到),比如:

從20億個數字的文本中,找出最大的前100個。

解決Top K問題有兩種思路,

  • 最直觀:小頂堆(大頂堆 -> 最小100個數);
  • 較高效:Quick Select算法。

LeetCode上有一個問題215. Kth Largest Element in an Array,類似於Top K問題。

1. 堆

小頂堆(min-heap)有個重要的性質——每個結點的值均不大於其左右孩子結點的值,則堆頂元素即為整個堆的最小值。JDK中PriorityQueue實現了數據結構堆,通過指定comparator字段來表示小頂堆或大頂堆,默認為null,表示自然序(natural ordering)。

小頂堆解決Top K問題的思路:小頂堆維護當前掃描到的最大100個數,其后每一次的掃描到的元素,若大於堆頂,則入堆,然后刪除堆頂;依此往復,直至掃描完所有元素。Java實現第K大整數代碼如下:

public int findKthLargest(int[] nums, int k) {
  PriorityQueue<Integer> minQueue = new PriorityQueue<>(k);
  for (int num : nums) {
    if (minQueue.size() < k || num > minQueue.peek())
      minQueue.offer(num);
    if (minQueue.size() > k)
      minQueue.poll();
  }
  return minQueue.peek();
}

2. Quick Select

Quick Select [1]脫胎於快排(Quick Sort),兩個算法的作者都是Hoare,並且思想也非常接近:選取一個基准元素pivot,將數組切分(partition)為兩個子數組,比pivot大的扔左子數組,比pivot小的扔右子數組,然后遞推地切分子數組。Quick Select不同於Quick Sort的是其沒有對每個子數組做切分,而是對目標子數組做切分。其次,Quick Select與Quick Sort一樣,是一個不穩定的算法;pivot選取直接影響了算法的好壞,worst case下的時間復雜度達到了\(O(n^2)\)。下面給出Quick Sort的Java實現:

public void quickSort(int arr[], int left, int right) {
  if (left >= right) return;
  int index = partition(arr, left, right);
  quickSort(arr, left, index - 1);
  quickSort(arr, index + 1, right);
}

// partition subarray a[left..right] so that a[left..j-1] >= a[j] >= a[j+1..right]
// and return index j
private int partition(int arr[], int left, int right) {
  int i = left, j = right + 1, pivot = arr[left];
  while (true) {
    while (i < right && arr[++i] > pivot)
      if (i == right) break;
    while (j > left && arr[--j] < pivot)
      if (j == left) break;
    if (i >= j) break;
    swap(arr, i, j);
  }
  swap(arr, left, j);  // swap pivot and a[j]
  return j;
}

private void swap(int[] arr, int i, int j) {
  int tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp;
}

Quick Select的目標是找出第k大元素,所以

  • 若切分后的左子數組的長度 > k,則第k大元素必出現在左子數組中;
  • 若切分后的左子數組的長度 = k-1,則第k大元素為pivot;
  • 若上述兩個條件均不滿足,則第k大元素必出現在右子數組中。

Quick Select的Java實現如下:

public int findKthLargest(int[] nums, int k) {
  return quickSelect(nums, k, 0, nums.length - 1);
}

// quick select to find the kth-largest element
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大元素;若想要得到Top K元素,僅需要將代碼做稍微的修改:比如,掃描完成后的小頂堆對應於Top K,Quick Select算法用中間變量保存Top K元素。

3. 參考資料

[1] Hoare, Charles Anthony Richard. "Algorithm 65: find." Communications of the ACM 4.7 (1961): 321-322.
[2] James Aspnes, QuickSelect.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM