1. 程式人生 > >資料結構演算法題/top K問題

資料結構演算法題/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));
        }
    }
    

參考https://www.cnblogs.com/en-heng/p/6336625.html