堆的應用
優先順序佇列,顧名思義,它首先應該是一個佇列。佇列最大的特性就是先進先出,而在優先順序佇列中,資料的出隊順序則是按照優先順序來,優先順序高的先出隊。
實現優先順序佇列的方法有很多,但是用堆來實現是最直接、最高效的。堆和優先順序佇列非常相似,一個堆就可以看作一個優先順序佇列。從優先順序佇列中取出優先順序最高的元素,就相當於取出堆頂元素。
1.1. 合併有序小檔案
假設我們有 100 個小檔案,每個檔案的大小是 100 MB,每個檔案中儲存的都是有序的字串,現在我們要將這些小檔案合併成一個有序的大檔案,就要用到優先順序佇列。
整體思路有點像歸併排序的合併操作。我們從這 100 個檔案中各自取出第一個字串放入到陣列中,然後比較大小,將最小的字串放入合併後的檔案,並從陣列中刪除。
假如這個最小的字串來自檔案 1.txt,那麼我們就再從這個檔案中取出下一個字串放入陣列,重新進行比較,找出最小的字串加入到大檔案中,然後從陣列中刪除。以此類推,直到我們遍歷完所有小檔案為止。
這裡,我們每次從陣列中找出最小的字串都要進行一遍遍歷。顯然,這不是很高效。
其實,我們就可以把陣列改成優先順序佇列,或者說是堆。我們把從小檔案中取出來的字串放入到小頂堆,那麼,堆頂的元素就是最小的也就是優先順序最高的元素。每次,我們都將堆頂元素放入到大檔案中並將其從堆頂刪除,然後再取出下一個字串放入堆中,直到所有小檔案都遍歷完畢即可。
刪除堆頂元素和往堆中插入資料的時間複雜度都為 , 代表堆中的資料個數,這裡就是 100,比陣列快多了。
1.2. 高效能定時器
假設我們有一個定時器,定時器中維護了很多定時任務,每個任務都設定了一個 要觸發執行的時間點。定時器每隔一個很小的時間單位(比如 1 秒)就會掃描一遍任務,看是否有任務需要執行。

但是,這樣做就非常低效。首先,如果距離任務執行時間點還太遠,那麼許多的掃描都是徒勞的。其次,每次我們都需要掃描整個任務列表,如果任務列表很大的話,就會比較耗時。
針對此,我們就可以按照任務執行時間的先後順序來建立一個優先順序佇列,優先順序最高的任務就是小頂堆的堆頂元素。
我們拿堆頂元素的執行時間點,與當前時間點相減,得到一個時間間隔 T。也就是說,從當前時間點再等待 T 時間,才有第一個任務需要執行。在這期間,我們就無需再查詢了。等到 T 時間後,我們取出堆頂任務執行,然後再重新計算差值,繼續等待。
2. 堆的應用二:利用堆求 Top K
求 Top K 的問題可以分為兩類。一類是靜態資料,資料集合事先知道,不會再變。另一類是動態資料,資料集合事先並不知道,有資料動態地加入到資料集合中。
針對靜態資料,我們可以維護一個大小為 K 的小頂堆。順序遍歷陣列,如果資料小於堆頂元素,則不作處理繼續遍歷;如果資料大於堆頂元素,則刪除堆頂元素並將當前資料插入到堆中。遍歷完陣列後,堆中資料即為前 K 大元素。
遍歷資料需要 的時間複雜度,而每一次堆操作需要 的時間複雜度,所以最壞情況下, 個元素都入堆,時間複雜度為 。
針對動態資料,我們可以一直維護一個大小為 K 的小頂堆,每當有新資料加入到集合中時,我們就拿它和堆頂元素進行比較,然後按照和上面靜態資料一樣的策略更新堆。這樣,無論任何時候需要查詢前 K 大數的時候,我們都可以直接返回隊中的元素即可。
3. 堆的應用三:利用堆求中位數
所謂中位數,就是處於中間位置的數字。如果資料的個數為奇數,那麼第 個數據就是中位數;如果資料的個數為偶數,那麼於中間位置的數字有兩個,我們可以取第 或第 個數據作為中位數。

對於靜態資料,我們可以先對資料進行排序,然後取出中間位置的資料即可。雖然排序的代價比較大,但是邊際成本會很小,我們只需要排序一次。
但是,針對動態資料,如果每次查詢中位數的時候都對資料進行排序,那效率就不高了。 藉助堆這種資料結構,我們不用排序,就可以非常高效地找到中位數 。
我們需要維護兩個堆,一個大頂堆,一個小頂堆。大頂堆中儲存前半部分資料,小頂堆中儲存後半部分資料,而且小頂堆中的資料都大於大頂堆中的資料。這時候,大頂堆的堆頂元素也就是我們要找的中位數。
如果是偶數情況,那麼大頂堆就有 個數據,小頂堆就有 個數據;如果是奇數情況,那麼大頂堆就有 個數據,小頂堆就有 個數據。

如果新加入的資料小於等於大頂堆的堆頂元素,我們就將這個資料插入到大頂堆;否則,我們就將這個資料插入到小頂堆。
這時候,就有可能會出現兩個堆中的資料個數不符合前面的約定。我們可以從一個堆中不停地將堆頂元素移動到另一個堆中,來讓兩個堆中的資料個數重新滿足上面的約定。

除此之外,我們還可以利用同樣的原理來快速求出其他百分位的資料。假如我們要找出介面的 99% 響應時間?
所謂 99% 的響應時間,就是對響應時間排完序後處於 99%*n 位置的資料。我們依然建立一個大頂堆和一個小頂堆,其中大頂堆中儲存 99%*n 的資料,而小頂堆中儲存 1%*n 的資料,然後依然按照上面處理中位數的操作對兩個堆進行維護。
ofollow,noindex">參考資料-極客時間專欄《資料結構與演算法之美》
獲取更多精彩,請關注「seniusen」!
