--- title: 'Heap 堆積' disqus: kyleAlien --- Heap 堆積 === ## OverView of Content 堆積跟 **優先序有關係**,有分最小堆積、最大堆積 [TOC] ## Heap 堆積 - 概述 Heap 的特色有: 1. 資料有優先順率 2. O(1) 的速度找到優先度最高的值 3. O(logN) 的速度移除最高優先度值 4. 以 O(N) 速度入隊列 ### Queue & Heap * Queue 是一種先進先出 (FIFO) 的結構,它可以達到 `enqueue`、`dequeue` 兩個行為皆是 O(1) 的效率,**但是如果 Queue 要判別優先度,`dequeue` 最差則會降至 O(N)** > 因為 `dequeue` 需要比較每個原素的優先序 ## 最大二元堆積 以下會以一個堆積最大尺寸 M (一般 Array),來儲存一個 N 大小的項目,其中 N < M ### 最大二元堆積 - 規則 * 下圖為一個最大二元堆積表示圖;其中 **二元堆積** 就是說每個節點最多只會有兩個子節點,並且填充必須從左到右填充 > 允許有相同優先度元素 > > ![](https://i.imgur.com/Z4ySn8F.png) 1. **堆積順序**:**Parent Node 必須比左、右 Node 都還要大,++左右誰大則無關++** 2. **堆積狀態**:**第 K 層必須滿足 2^k^ 大小**,才能處理 K + 1 層的項目 > 如果要求總數量 (非完整二元),有 K 層,則總數會在 (2^k^) ~ (2^K+1^ - 1) 之間 > > ![](https://i.imgur.com/boV6l00.png) * 有 N 個項目時 Heap 需要幾層才能完成存取 ? 這就跟 **log 對數** 有關 有 N 個項目時,需要 log(N) 層 (這是以根節點為第 0 層開始算);N = 7 則 log(7) = 2.8,取整則為 2;N = 8 則 log(8) = 3,本身整數,則為 3 > log 使用 2 為底數 > ![](https://i.imgur.com/9cCKqCo.png) ### Heap 操作 - 插入 enqueue 上浮 * Heap 若是要插入新元素,則須將新元素放置對列尾部,透過 **++上浮操作++** (也就是向上層逐一比對交換),最終得到結果 > ![](https://i.imgur.com/Bcyahmb.png) 以下為插入新元素 `14` 的上浮比較順序;上浮操作也有幾個需要注意的特點 1. 比較 14 > 11 ? 兩者交換,繼續上浮比較 > ![](https://i.imgur.com/ZS3BBEn.png) 2. 比較 14 > 13 ? 兩者交換,繼續上浮比較 > ![](https://i.imgur.com/vENTDGz.png) :::info * 這裡有個特點,需要關心另外一個子樹 9 的項目需要交換嗎 ? 不需要! 因為它已經按照規則進行過排序,代表 **9 已經小於原本的 Parent Node,所以就算新交換上去也可以正常運作** ::: 3. 比較 14 > 15 ? 不交換,最終完成 `enqueue` 整理 > ![](https://i.imgur.com/FznwcJc.png) :::success * **二元堆積,插入 `enqueue` 效能分析**: 有沒有發現 **它比較的次數就是 Heap 的高度**! 我們前面已經有分析過,Heap 高度就是 `O(logN)`,因此 **二元堆積 `enqueue` 的效能也同樣是 `O(logN)`** ::: ### Heap 操作 - 移除 dequeue 下沉 * 移除最高優先度元素,其實就是取出 Heap 最前面的元素,但在之後仍需整理 Heap 列表 (不能讓 Header 為空),其步驟如下 1. 移除 Heap 中最後一個元素,之後會替換第 0 層 (Header) 2. 儲存第 0 層 (Header) 最高優先項目的序號,方便之後回傳 3. 重整 Heap 列表 * 移除 `dequeue` 順序概念圖如下 1. 移除最尾端元素 11 > ![](https://i.imgur.com/CoVR3Uh.png) 2. 儲存第 0 層 (Header) 最高優先項目 > ![](https://i.imgur.com/ii7GMyY.png) 3. 重整 Heap 列表 * 判斷左、右子樹,誰的優先序高則與其交換;相同、只有左子樹則與左邊交換交換一個 ( 相等時若與右邊交換則會導致樹不平衡 比較兩個子樹 14 == 14,挑選左邊進行交換 > ![](https://i.imgur.com/NGyvYTH.png) 比較兩個子樹 13 > 9,挑選左邊進行交換 > ![](https://i.imgur.com/53pcIcQ.png) :::warning * 如果比較項目比左、右都小,則需 **比較左右大小,挑選大的交換 !!** ::: :::success * **二元堆積,移除 `dequeue` 效能分析**: **它的移動的次數就是 Heap 的高度 `O(log(N))`**,但比較次數則是 `O(2*log(N))` (比較左右子樹大小),所以 **移除 `dequeue` 效能仍屬於 `O(log(N))` 的效能** ::: ## Array Heap 這裡為了簡化運算,會將 Array[0] 的位置空下,所以總數組長度為 Array.length > ![](https://i.imgur.com/is40zNr.png) * 上圖可以雖算出 Parent Node、Child Node 之間的關係,並用數學式表示;假設 Array 名稱為 storage[] 1. 父推子:求節點 storage[K] 的左右子節點 * **左 `storage[K * 2]`**;假設 K 為 3,左節點為 storage[6],值就是 12 * **右 `storage[K * 2 + 1]`**;假設 K 為 4,右節點為 storage[9],值就是 2 > ![](https://i.imgur.com/fBFMSm2.png) 2. 子推父:求節點 storage[K] 的左右子節點 * **右、左 `storage[K / 2]` (無條件捨去)**; 假設 K 為 7,父節點為 storage[3],值就是 14 假設 K 為 6,父節點為 storage[3],值就是 14 > ![](https://i.imgur.com/GTWggpi.png) 3. **index K 有效範圍是 0 <= K <= N**:超出該範圍,K 無效,也就超出 Heap ### enqueue 實作 - swim 上浮 * 接下來我們實作 enqueue 時,可能發生的上浮行為,細節請看註解 ```java= public class Heap { private final Integer[] storage; private int cur; // 用來記錄當前儲存的元素數量 public Heap(int size) { storage = new Integer[size + 1]; // + 1 是因為我們會空下 [0] cur = 0; } public void enqueue(int newVal) { if(cur == storage.length - 1) { // 容量只有原大小 - 1 throw new RuntimeException("Array already full."); } cur += 1; storage[cur] = newVal; // 放入數組最後 swim(cur); } public void swim(int index) { while (index > 1) { // > 1 是因為我們空下 [0] // 算出父節點 index int compareIndex = index / 2; int compareVal = storage[compareIndex]; // 與父節點數值做比較,小於父節點就代表上面的也不用再交換 if(storage[index] < compareVal) { break; } // 交換數值 swap(index, compareIndex); index = compareIndex; } } // 交換 i & j 的數值 public void swap(int i, int j) { int tmp = storage[i]; storage[i] = storage[j]; storage[j] = tmp; } public static void main(String[] args) { // 已排列好的 Heap int[] a = {15, 13, 14, 9, 11, 12, 14, 8, 2, 6}; Heap heap = new Heap(a.length); for (int i : a) { heap.enqueue(i); System.out.println("Array: " + Arrays.toString(heap.storage)); } } } ``` > ![](https://i.imgur.com/S6PZeqC.png) :::success * 其中我們可以保證 `swap` 函數的執行次數必定不會大於 `log(N)` (N 為 Heap 數量) ::: ### dequeue 實作 - sink 下沉 * 接下來我們實作 dequeue 時,可能發生的下沉行為,細節請看註解 ```java= public class Heap { private final Integer[] storage; private int cur; // 用來記錄當前儲存的元素數量 public Heap(int size) { storage = new Integer[size + 1]; // + 1 是因為我們會空下 [0] cur = 0; } ... 省略 enqueue public void swap(int i, int j) { int tmp = storage[i]; storage[i] = storage[j]; storage[j] = tmp; } public int dequeue() { if(cur == 0) { // 判斷是否有元素可取 throw new RuntimeException("No value in heap now."); } int top = storage[1]; // 保留 top 值 storage[1] = storage[cur]; // 最後元素放置第一,開始準備下沉 storage[cur] = null; // 將原本最後的元素至為 null cur -= 1; // 減去 cur sink(); return top; } public void sink() { int rootIndex = 1; // 從頂端下沉 while (rootIndex * 2 + 1 <= cur) { // 判斷是否越界 int rootVal = storage[rootIndex]; // root 取左右 int leftIndex = 2 * rootIndex; int rightIndex = 2 * rootIndex + 1; int swapIndex = leftIndex; // 預設為左 // 比較左右,取最大的 index if(storage[leftIndex] < storage[rightIndex]) { swapIndex = rightIndex; // 發現右邊較大,則換右的 index } // 判斷 root 是否比要交換的元素大 if(rootVal > storage[swapIndex]) { // root 比較大,則後面不需要再交換 break; } swap(rootIndex, swapIndex); // 交換 rootIndex = swapIndex; } } public static void main(String[] args) { int[] a = {15, 13, 14, 9, 11, 12, 14, 8, 2, 6}; Heap heap = new Heap(a.length); for (int i : a) { heap.enqueue(i); } for (int j = 1; j < heap.storage.length; j++) { int top = heap.dequeue(); System.out.println("top: " + top); System.out.println("Array: " + Arrays.toString(heap.storage)); } } } ``` 從結果可以看出,它從 **大到小** 自動排出 > ![](https://i.imgur.com/PuuWF2N.png) :::success * 其中我們可以保證 `sink` 函數的執行次數必定不會大於 `log(N)` (N 為 Heap 數量) ::: ### 幾何大小調整 * 如同雜湊表 HashTable 一樣,可以使用幾何的方式調整列表,在調整完後 **將原數據重新入 Heap 列表** ```java= public void enqueue(int newVal) { if(cur == storage.length - 1) { // 進行大小調整 resize(); } cur += 1; storage[cur] = newVal; swim(cur); } public void resize() { int oldSize = storage.length; Integer[] tmp = storage; storage = new Integer[oldSize * 2]; // 幾何大小調整 cur = 0; for(int i = 1; i < tmp.length; i++) { if(tmp[i] == null) continue; enqueue(tmp[i]); } } ``` :::info * 這裡就不關注 threshold 伐值,直接進行幾何大小調整 * **再次入隊 `enqueue` 的 BIG O 仍為 O(log (N))** ::: ## Appendix & FAQ :::info ::: ###### tags: `資料結構`