深入理解 TOP K問題
前面一片文章提過,完全二叉樹非常適合用陣列這種資料結構來實現。所以堆作為一個完全二叉樹肯定用陣列來實現最合適。
而且規律也很簡單,我們再總結一遍就是:
如果一個節點的下標為i,那麼他的左子節點的下標就是2i,右子節點的下標就是2i+1
父節點就是i/2.
還不理解上述 完全二叉樹---陣列 這種對映關係的 可以看前一篇文章或者自己畫一畫。
那實現一個堆無非就是要完成對這個堆的 插入資料和刪除資料操作。
插入演算法
插入操作真的蠻簡單的,我們可以看一下剛才那張圖,以這個大頂堆為例。 假設我們插入一個值,我們先把這個值 插入在這個 堆的 末尾位置,然後 將這個值 與他的父節點對比,如果比父節點大 那麼就交換位置,如果比父節點小 那麼就插入完畢。
比方說我們對大頂堆插入一個10的資料,

可以看出來 這個 自下而上 的插入演算法還是很好理解的。
刪除堆頂元素演算法
根據大頂堆和小頂堆的定義可以得知,堆頂元素永遠都是最大或者最小的。那麼刪除堆頂元素以後,新的堆頂元素的值 自然就是 原來堆頂元素的左兒子和右兒子了。
所以顯然我們天真的以為刪除堆頂元素的演算法應該是(假設是大頂堆):
刪除堆頂元素以後,我們將左兒子的值和右兒子的值比較,誰的值比較大,那麼就去堆頂呆著。 等兒子去了堆頂位置以後,
兒子的兒子也是一樣進行如上操作 看誰大就往上挪一個位置。 看起來這個演算法無懈可擊也好理解,但是這種演算法容易
出現下述異常情況。

所以看的出來這裡的演算法會導致某種情況下 堆的完全二叉樹被破壞掉。
所以我們要將其改進一下,改進後的演算法如下:
要刪除堆頂元素的時候,先刪除堆頂元素然後把堆的最後一個位置的資料放到堆頂,然後自上而下的比對大小,交換位置。
我們來看看,這種演算法的圖解:

顯然這種演算法 就會避免第一種演算法帶來的陣列空洞問題。
傳說中的堆排序就是用這個 堆來實現的嗎,具體怎麼做的?
是的,堆排序中的堆就是用這個堆來實現資料結構的。實現起來稍微複雜了一丟丟,簡單說一下吧(不詳細說是因為堆排序實在沒啥人用):
給定一個無序的陣列,問,如何用堆這種資料結構來進行排序?
首選把這個無序陣列進行建堆的操作,前面說過了插入一個數據的演算法,那麼對於這個無序陣列來說,我們可以假定陣列的第一個元素就是堆頂的位置,然後把後面的元素依次按照我們的插入演算法 插入即可。(實際上有更加友好的建堆方法,但這不是今天重點,有興趣的同學可以自己搜尋一下)
有了這個堆以後,如何進行排序呢? 假設是大頂堆,其實對這個堆進行排序就好像我們的刪除堆頂元素的演算法, 你刪了一個堆頂元素以後,第二打的元素不就到對頂了麼?以此類推。。。
堆排序為啥沒人用呢?
實際上堆排序效能看起來“似乎還可以”,具體時間複雜度和快排序是一樣的,時間複雜度都是O(nlogn),而且還是穩定排序演算法。 但是這東西有個致命的缺陷:
堆的操作太多依賴於交換操作,看起來時間複雜度和快排序一樣,但是因為你交換次數太多了,所以實際表現遠遠不如快排序。
就好比舉重比賽一樣,大家都是一樣的成績,你比我重一點,當然冠軍是我。
羅嗦半天堆排序這麼垃圾那你還講他幹嘛?
天生我才必有用啊,兄弟,堆這東西 幹純排序不行,其他東西可是有妙用的。
優先順序佇列 幾乎就等於 “堆”
所謂優先順序佇列就是,不管進入的順序如何,出去的順序 是按照優先順序來的,仔細想想是不是和我們的大頂堆或者小頂堆的定義 無限趨於一致?熟悉android看過volley原始碼的同學應該都知道,volley裡面的PriorityBlockingQueue 不就是一個執行緒安全的 用堆這種資料結構來構成的優先順序佇列麼?
再來看經典的TOP K 問題
有了上述基礎,我們再來看TOP K問題就簡單許多了。leetcode 703 題: ofollow,noindex">leetcode-cn.com/problems/kt…
簡單來說,就是給定你一個數組,每次插入資料的時候,要你返回這個數組裡面第K大的元素。
下面來說說這道問題的解法思路:
1。 陣列中每次插入一個數據的時候,我們都進行快排序,然後取出第k個大小就可以。這是思路上最簡單但是最蠢的方法。 因為每次插入一個數據都要全部排序 那也太慢了。
- 在知道有優先順序佇列這麼個東西以後,我們就可以維持一個 k大小的 小頂堆,每次有新元素進來的時候 我們就看看這個新元素 的值 是不是比這個小頂堆的值要大,如果比他大 那麼就把這個小頂堆的堆頂元素刪掉,然後插入我們這個新元素的值以後, 再取這個小頂堆的堆頂元素 就是我們的第k大元素了。
class KthLargest { PriorityQueue<Integer> queue; int maxHeapSize; public KthLargest(int k, int[] nums) { //初始化一個大小為k的小頂堆 queue = new PriorityQueue<Integer>(k); maxHeapSize=k; if (k < nums.length) { //先構造一個大小為k的 堆 for (int i = 0; i < k; i++) { queue.add(nums[i]); } //下面構造的時候要判斷大小 for (int j = k; j < nums.length; j++) { if (nums[j] > queue.peek()) { queue.poll(); queue.offer(nums[j]); } else { } } } else { for (Integer integer : nums) { queue.add(integer); } } } public int add(int val) { if(queue.size()<maxHeapSize) { queue.offer(val); return queue.peek(); } if (val > queue.peek()) { queue.poll(); queue.offer(val); } return queue.peek(); } } /** * Your KthLargest object will be instantiated and called as such: * KthLargest obj = new KthLargest(k, nums); * int param_1 = obj.add(val); */ 複製程式碼
看下效果:

嗯 還可以。
然後最後看下leetcode 239問題 leetcode-cn.com/problems/sl…
有了優先順序佇列以後這個問題也變的簡單了吧。只要遍歷陣列的時候維護一個大頂堆,問題就迎刃而解了。(程式碼就不上了,很簡單)
當然其實這個問題還有更好的解法,就是不用堆,而用雙端佇列,有興趣的同學可以研究下。
堆或者說優先順序佇列還能解決什麼實際生產問題嗎?
其實能解決的生產問題很多,比方說,我們做Android定製系統的,假設使用者搞了N個任務在那裡,每個任務都有特定的執行時間, 到了時間就要執行使用者的任務,比如說鬧鐘。那我們怎麼完成這個需求呢?
每過一秒就檢查 任務列表 有沒有時間跟當前時間符合的嗎?顯然不行,這樣效率太低。
我們可以把這些任務儲存成為一個小頂堆,這樣只要系統時間過一秒,我們就看看是不是和堆頂元素時間一樣即可。