用 Heap 實作 Priority Queue
Priority Queue
面對同性質的許多筆記錄時 (例如數百位申請獎助學金的學生的學籍資料), 我們會需要一個可以收納這些記錄的大型資料結構。 用 c++ 的術語來說, 這類具收納功能的資料結構都叫做 container。 例如陣列, linked list, 集合, ... 等等都是 containers。
有時候我們最常做的動作, 是從一個 container 當中固定取出其中 「優先順序最高」 少數幾筆資料 (例如根據複雜公式計算出來的獎助優先順序)。 當然我們也可能需要新增資料, 甚至可能改變其中某一筆資料的 「優先順序」。 一個 container (姑且稱之為 PQ), 除收納功能以外, 如果還提供以下功能, 就稱為一個 priority queue:
- insert: 新增一筆記錄進入 PQ。
- remove: 傳回 「優先順序」最高 的那一筆記錄, 並將它從 PQ 當中刪除。
- change: 改變 PQ 裡面一筆記錄的「優先順序」。
- is_empty: 查詢 PQ 裡面是否已清空, 沒有任何記錄。
繼續以 c++ 的術語來說, priority queue 其實是一個 abstract class -- 它不像二元樹或 doubly linked list 等等那麼清楚地有明確的結構, 它只是一個抽象的介面 interface。 有許多不同的實際資料結構, 都可以拿來提供上述功能, 也就是說, 我們想實作 implement 出一個 priority queue, 其實有許多資料結構可以用, 例如 unsorted array, sorted array, unsorted linked list, sorted linked list。 請仔細思考用每一種資料結構實作時, 每個功能該如何實作; 各需要多少時間。 然後與下表對一下答案。
insert | remove | change | is_empty | search | |
unsorted array | O(1) | O(n) | srch+O(1) | O(1) | O(n) |
sorted array | O(n) | O(n) | O(n) | O(1) | O(lg n) |
circular sorted array | O(n) | O(1) | O(n) | O(1) | O(lg n) |
unsorted linked list | O(1) | O(n) | srch+O(1) | O(1) | O(n) |
sorted linked list | O(n) | O(1) | O(n) | O(1) | O(n) |
這裡的 "srch+" 表示搜尋的時間。 如果已知要改變優先順序的元素在那裡, srch=0; 否則 srch 就是 search 欄的時間。
circular sorted array 是 sorted array 的改進版, 有點像是 circular queue。 要把它的 search 寫成 O(lg n), 需要一些功夫。
Priority Queue 可以拿來排序。 用下面這個演算法:
void pqsort() { for (i=0; i<n; ++i) pq.insert(data[i]); while (! pq.is_empt()) { cout << pq.remove(); } }
其實如果你用 sorted array/list 或是 unsorted array/list 來實作上述演算法當中的 priority queue, 那麼上面的 pqsort 其實就變成了 insertion sort 或是 selection sort (Q: 仔細想想看, 那個是那個?) 這個演算法的複雜度很容易算: 兩個迴圈各做 n 次, 所以如果 insert, remove, is_empty 等三個動作之中最耗時的要花 n, 那麼整體所花的時間就是 n^2。
仔細分析 pqsort 的耗時為 O(n * (新增耗時 + 刪除耗時))。 又從上表看出不同的資料結構, 有些新增快/刪除慢, 有些新增慢/刪除快。 天下沒有十全十美的事, 這是權衡取捨的結果。 但是權衡取捨之下, 是否可能設計出一個資料結構, 它的新增耗時與刪除耗時差不多, 雖然都比 O(1) 慢, 但也都比 O(n) 快呢? 如果可以的話, 那麼我們立即就多了一個比 insertion sort 與 selection sort 都快的排序演算法了。
Heap
想要只花 log n 的時間來增/刪/查一筆記錄, 這個資料結構的長相應該與矮胖的樹有關。 Q: 一個具有 n 個 nodes 的 full binary tree, 它的高度是多少? (你學過 big-O, 現在可以拿出來偷懶) 當然記錄的筆數未必那麼剛好是 2 的次方減 1, 如果我們有 38 筆或是 47 筆資料, 會希望放入什麼樣的樹當中呢? 退而求其次, 可以把樹的上面 h-1 層補滿, 然後最下面不足一層的所有 nodes 由左而右依次補滿, 右邊不足處全部放空, 就成為一棵 complete binary tree。 (所以 full binary tree 是 complete binary tree 的特例)
滿足以下兩個條件的資料結構, 稱為 heap:
- 外觀是一棵 complete binary tree
- 裡面所有元素皆符合 heap condition
所謂 heap property (或稱 heap condition) 是指每個 node 內的資料比它左右兩側 child nodes 內的資料都小 (但左右兩 child nodes 之間並無一定的關係)。 換句話說, 每棵子樹當中, 最小的元素總是在樹根。
雖說 heap 在觀念上是一棵 complete binary tree, 實際上是存在一個陣列當中 -- root 存在 A[1], 接下來將 A[2] 與 A[3] 由左到右依序補滿第二層, 再將 A[4], A[5], A[6], A[7] 由左到右依序補滿第三層, ... Q: A[25] 的兩個 children 是誰? A[93] 的 parent 是誰? 這種儲存方式完全不需要用到指標。
一個 heap 內如果有一個元素 x 的資料值改變, 因而違反了 heap property, 我們可以用下列兩個演算法之一將這個 heap 修復:
- upheap: 如果 x 比它的 parent 小, 則應該將它一路往上浮 (與 parent 交換位置), 直到它比新的 parent 大為止。 耗時 O(lg n)。
- downheap: 如果 x 比它的 child 大, 則應該將它一路往下沉 (與其中一個 child 交換位置), 直到它比兩個新的 children 都小為止。 究竟應該與左右那一個 child 交換位置呢? 為了要維持交換後的 heap property, 應該與比較小的 child 交換。 耗時 O(lg n)。
如何從 heap 當中移除最小的元素? 把 root 移除, 把整棵樹最右下角的元素搬到 root, 再用 downheap 修復。
如何加入一個 (任意的) 元素到 heap 當中: 加到整棵樹最右下角的空位, 再用 upheap 修復。
建立 heap 的動作, 可以逐一 insert 的方式達成, 但是這樣耗時 O(n log n); 這裡介紹比較快的方法。 先把所有元素不分順序直接放入 heap 中。 一開始將整個陣列的後半部看成是一大堆小 heaps, 逐層由下往上建立稍大的 heaps, 最後建立出一個完整的大 heap。 仔細翻譯成程式碼, 發現用 for (i=n/2; i>0; --i) downheap(i) 的順序正好可以達成這個效果。 耗時多少? 最開始的 n/2 個元素各耗時 1; 其次的 n/4 個元素各耗時 2; 再來的 n/8 個元素各耗時 3 ... 共耗時 O(n)
「最小元素放 root」 的 heap, 叫做 min-heap; 「最大元素放 root」 的 heap, 叫做 max-heap。
如上述, 如果 pqsort() 當中所用的資料結構是 heap, 那麼這個排序演算法就叫做 heapsort。 如果我們的 heap 主要只是為了拿來排序, 那麼不如改用 max-heap。 Heap 建立好之後, 最大元素正好在 root (陣列的第一個元素), 把它和最後一個元素對調, 想像 heap 縮小一格, 而排序完畢的陣列正好從尾巴開始逐漸出現。 這樣 heapsort 就變成是 in-place 了。
請在命令列下執行: perl Heap.pm
觀察它的輸出。 首先是建立 heap 的過程, 由下而上, 每完成一層的
downheap, 就將整個 heap 列印一次。 其次是隨意新增幾個元素。
最後將所有元素逐一刪除。 你可以修改程式下方 $data
變數的初始值, 甚至可以改成用亂數產生, 用不同的資料跑跑看。
- 本頁最新版網址: https://frdm.cyut.edu.tw/~ckhung/b/al/heap.php; 您所看到的版本: February 14 2012 10:32:24.
- 作者: 朝陽科技大學 資訊管理系 洪朝貴
- 寶貝你我的地球, 請 減少列印, 多用背面, 丟棄時做垃圾分類。
- 本文件以 Creative Commons Attribution-ShareAlike License 或以 Free Document License 方式公開授權大眾自由複製/修改/散佈。