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.