什麼是 Top K 問題?簡單來講就是在一堆數據裏面找到前 K 大(固然也能夠是前 K 小)的數。面試
這個問題也是十分經典的算法問題,不管是面試中仍是實際開發中,都很是典型。而這個問題其實也有不少種作法,你真的都懂了麼?算法
既然是要前 K 大的數,那麼最直接的固然就是排序了,經過如快排等效率較高的排序算法,能夠在平均 O(nlogn)的時間複雜度找到結果。數組
這種方式在數據量不大的時候簡單可行,但當然不是最優的方法。bash
剛剛提到了快排,熟悉算法題的小夥伴應該知道,快排的 partition 劃分思想能夠用於計算某個位置的數值等問題,例如用來計算中位數;顯然,也適用於計算 TopK 問題分佈式
每次通過劃分,若是中間值等於 K ,那麼其左邊的數就是 Top K 的數據; 固然,若是不等於,只要遞歸處理左邊或者右邊的數便可大數據
該方法的時間複雜度是 O(n) ,簡單分析就是第一次劃分時遍歷數組須要花費 n,而日後每一次都折半(固然不是準確地折半),粗略地計算就是 n + n/2 + n/4 +... < 2n,所以顯然時間複雜度是 O(n)ui
對比第一個方法顯然快了很多,隨着數據量的增大,兩個方法的時間差距會愈來愈大spa
雖然時間複雜度是 O(n) ,可是缺點也很明顯,最主要的就是內存問題,在海量數據的狀況下,咱們頗有可能沒辦法一次性將數據所有加載入內存,這個時候這個方法就沒法完成使命了3d
還有一點就是這種思路須要咱們修改輸入的數組,這也是值得考慮的一點code
面對海量數據,咱們就能夠放分佈式的方向去思考了
咱們能夠將數據分散在多臺機器中,而後每臺機器並行計算各自的 TopK 數據,最後彙總,再計算獲得最終的 TopK 數據
這種數據分片的分佈式思想在面試中很是值得一提,在實際項目中也十分常見
其實提到 Top K 問題,最經典的解法仍是利用堆。
維護一個大小爲 K 的小頂堆,依次將數據放入堆中,當堆的大小滿了的時候,只須要將堆頂元素與下一個數比較:若是大於堆頂元素,則將當前的堆頂元素拋棄,並將該元素插入堆中。遍歷徹底部數據,Top K 的元素也天然都在堆裏面了。
固然,若是是求前 K 個最小的數,只須要改成大頂堆便可
對於海量數據,咱們不須要一次性將所有數據取出來,能夠一次只取一部分,由於咱們只須要將數據一個個拿來與堆頂比較。
另外還有一個優點就是對於動態數組,咱們能夠一直都維護一個 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;
}
複製代碼