經典的 Top K 問題,你真的懂了麼?

Howie_Y發表於2019-01-28

什麼是 Top K 問題?簡單來說就是在一堆資料裡面找到前 K 大(當然也可以是前 K 小)的數。

這個問題也是十分經典的演算法問題,不論是面試中還是實際開發中,都非常典型。而這個問題其實也有很多種做法,你真的都懂了麼?

一. 立刻就能想到的解法

既然是要前 K 大的數,那麼最直接的當然就是排序了,通過如快排等效率較高的排序演算法,可以在平均 O(nlogn)的時間複雜度找到結果。

這種方式在資料量不大的時候簡單可行,但固然不是最優的方法。

二. O(n) 時間複雜度的方法

剛剛提到了快排,熟悉演算法題的小夥伴應該知道,快排的 partition 劃分思想可以用於計算某個位置的數值等問題,例如用來計算中位數;顯然,也適用於計算 TopK 問題

經典的 Top K 問題,你真的懂了麼?

每次經過劃分,如果中間值等於 K ,那麼其左邊的數就是 Top K 的資料; 當然,如果不等於,只要遞迴處理左邊或者右邊的數即可

該方法的時間複雜度是 O(n) ,簡單分析就是第一次劃分時遍歷陣列需要花費 n,而往後每一次都折半(當然不是準確地折半),粗略地計算就是 n + n/2 + n/4 +... < 2n,因此顯然時間複雜度是 O(n)

對比第一個方法顯然快了不少,隨著資料量的增大,兩個方法的時間差距會越來越大

缺點

雖然時間複雜度是 O(n) ,但是缺點也很明顯,最主要的就是記憶體問題,在海量資料的情況下,我們很有可能沒辦法一次性將資料全部載入入記憶體,這個時候這個方法就無法完成使命了

還有一點就是這種思路需要我們修改輸入的陣列,這也是值得考慮的一點

三. 利用分散式思想處理海量資料

面對海量資料,我們就可以放分散式的方向去思考了

我們可以將資料分散在多臺機器中,然後每臺機器平行計算各自的 TopK 資料,最後彙總,再計算得到最終的 TopK 資料

這種資料分片的分散式思想在面試中非常值得一提,在實際專案中也十分常見

四. 利用最經典的方法,一臺機器也能處理海量資料

其實提到 Top K 問題,最經典的解法還是利用堆。

維護一個大小為 K 的小頂堆,依次將資料放入堆中,當堆的大小滿了的時候,只需要將堆頂元素與下一個數比較:如果大於堆頂元素,則將當前的堆頂元素拋棄,並將該元素插入堆中。遍歷完全部資料,Top K 的元素也自然都在堆裡面了。

當然,如果是求前 K 個最小的數,只需要改為大頂堆即可

將資料插入堆

95 大於 20,進行替換

95 下沉,維持小頂堆

對於海量資料,我們不需要一次性將全部資料取出來,可以一次只取一部分,因為我們只需要將資料一個個拿來與堆頂比較。

另外還有一個優勢就是對於動態陣列,我們可以一直都維護一個 K 大小的小頂堆,當有資料被新增到集合中時,我們就直接拿它與堆頂的元素對比。這樣,無論任何時候需要查詢當前的前 K 大資料,我們都可以裡立刻返回給他。

整個操作中,遍歷陣列需要 O(n) 的時間複雜度,一次堆化操作需要 O(logK),加起來就是 O(nlogK) 的複雜度,換個角度來看,如果 K 遠小於 n 的話, O(nlogK) 其實就接近於 O(n) 了,甚至會更快,因此也是十分高效的。

最後,對於 Java,我們可以直接使用優先佇列 PriorityQueue 來實現一個小頂堆,這裡給個程式碼:

public List<Integer> solutionByHeap(int[] input, int k) {
    List<Integer> list = new ArrayList<>();
    if (k > input.length || k == 0) {
        return list;
    }
    Queue<Integer> queue = new PriorityQueue<>();
    for (int num : input) {
        if (queue.size() < k) {
            queue.add(num);
        } else if (queue.peek() < num){
            queue.poll();
            queue.add(num);
        }
    }
    while (k-- > 0) {
        list.add(queue.poll());
    }
    return list;
}
複製程式碼

相關文章