# 用Java學資料結構-堆Heap ```mermaid graph TD A[50]-->B[30] A-->C[40] B-->D[10] B-->E[20] C-->F[35] C-->G[25] style A fill:#f96,stroke:#333 style B fill:#9cf,stroke:#333 style C fill:#9cf,stroke:#333 ``` ## 什麼是堆 (Heap) ### 堆的基本概念 #### 堆的定義 - 堆的特性(最大堆與最小堆) - 最大堆:每個節點的值都大於或等於其子節點的值 ```mermaid graph TD subgraph 最大堆 A[100]-->B[80] A-->C[90] B-->D[70] B-->E[75] end ``` - 最小堆:每個節點的值都小於或等於其子節點的值 ```mermaid graph TD subgraph 最小堆 A[10]-->B[20] A-->C[15] B-->D[40] B-->E[30] end ``` - 完全二元樹的結構 - 除了最底層,其他層的節點都必須是滿的 ```mermaid graph TD A[50]-->B[30] A-->C[40] B-->D[10] B-->E[20] ``` - 最底層的節點從左到右填入,不能有空隙,像底下這樣就是一個不合法的完全二元樹,在最底層沒有從左到右填入,且出現了空隙(指沒有子節點的地方) ```mermaid graph TD A[50]-->B[30] A-->C[40] B-->E[10] C-->F[20] ``` ## 堆的應用場景 ### 堆的優勢 - 堆的插入與刪除操作的時間複雜度都是 O(logN) > O(logN):隨著元素數量 N 的增加,時間複雜度增加的速度是對數級別的,也就是說當 N 翻倍時,時間複雜度只增加一個常數倍數,例如 N=2 時,時間複雜度是 1,N=4 時,時間複雜度是 2,N=8 時,時間複雜度是 3,以此類推 - 堆可以快速找到最大值或最小值 #### 堆的常見使用情境 - 任務調度有順序且有優先分級需求時,可以使用堆來實現優先佇列 - 資料流中找出前 K 大或前 K 小的元素 - 合併多個有序陣列 ### 堆的限制 - 當需要頻繁地查找中間元素時,時間複雜度是 O(N) > O(N):隨著元素數量 N 的增加,時間複雜度增加的速度是線性的,也就是說當 N 翻倍時,時間複雜度也會翻倍,例如 N=2 時,時間複雜度是 2,N=4 時,時間複雜度是 4,N=8 時,時間複雜度是 8,以此類推 - 佔用的記憶體空間較大 #### 不適合使用堆的情況 - 當需要頻繁地查找中間元素時 - 需要快速查找元素的索引時 - 需要有序遍歷所有元素時 - 資料量較小時 ## 使用 Java 實現堆 我們來使用 Java 來實作堆,首先我們需要定義堆的基本操作 - 獲取父節點的索引 - 獲取左子節點的索引 - 獲取右子節點的索引 - 交換左右子節點的值 - 上浮操作,上浮指的是將某個節點的值與其父節點的值進行比較,如果不符合堆的特性(最大堆或最小堆),則交換兩者的值,直到滿足堆的特性 - 下沉操作,下沉指的是將某個節點的值與其子節點的值進行比較,如果不符合堆的特性(最大堆或最小堆),則交換兩者的值,直到滿足堆的特性 - 插入元素 - 刪除堆頂並返回新的堆頂元素 - 查看堆頂元素 - 獲取堆的大小 - 判斷堆是否為空 ```java public class Heap { private int[] elements; private int size; private boolean isMaxHeap; // 結構子 public Heap(int capacity, boolean isMax) { elements = new int[capacity]; size = 0; isMaxHeap = isMax; } // 獲取父節點索引 private int parent(int index) { return (index - 1) / 2; } // 獲取左子節點索引 private int leftChild(int index) { return 2 * index + 1; } // 獲取右子節點索引 private int rightChild(int index) { return 2 * index + 2; } // 交換左右子節點的值 private void swap(int i, int j) { int temp = elements[i]; elements[i] = elements[j]; elements[j] = temp; } // 上浮操作 private void heapifyUp(int index) { while (index > 0) { int parent = parent(index); if (isMaxHeap) { if (elements[index] > elements[parent]) { swap(index, parent); index = parent; } else { break; } } else { if (elements[index] < elements[parent]) { swap(index, parent); index = parent; } else { break; } } } } // 下沉操作 private void heapifyDown(int index) { while (true) { int largest = index; int left = leftChild(index); int right = rightChild(index); if (isMaxHeap) { if (left < size && elements[left] > elements[largest]) { largest = left; } if (right < size && elements[right] > elements[largest]) { largest = right; } } else { if (left < size && elements[left] < elements[largest]) { largest = left; } if (right < size && elements[right] < elements[largest]) { largest = right; } } if (largest == index) { break; } swap(index, largest); index = largest; } } // 插入元素 public void insert(int value) { if (size >= elements.length) { throw new IllegalStateException("Heap is full"); } elements[size] = value; heapifyUp(size); size++; } // 刪除堆頂元素並返回新的堆頂元素 public int poll() { if (size == 0) { throw new IllegalStateException("Heap is empty"); } int result = elements[0]; elements[0] = elements[--size]; if (size > 0) { heapifyDown(0); } return result; } // 查看堆頂元素 public int peek() { if (size == 0) { throw new IllegalStateException("Heap is empty"); } return elements[0]; } // 獲取堆的大小 public int size() { return size; } // 判斷堆是否為空 public boolean isEmpty() { return size == 0; } // 自下而上建立堆 public void buildHeapBottomUp(int[] array) { // 複製數組到堆中 System.arraycopy(array, 0, elements, 0, array.length); size = array.length; // 從最後一個非葉節點開始向下堆化 for (int i = size/2 - 1; i >= 0; i--) { heapifyDown(i); } } // 自上而下建立堆 public void buildHeapTopDown(int[] array) { size = 0; // 逐個插入元素 for (int value : array) { insert(value); } } // 堆排序 public int[] heapSort() { int[] result = new int[size]; int originalSize = size; for (int i = 0; i < originalSize; i++) { result[i] = poll(); // 對於最小堆,將得到升序排序 } return result; } public static int[] findTopK(int[] nums, int k, boolean findMax) { Heap heap = new Heap(k, !findMax); // 找最大用最小堆,找最小用最大堆 for (int num : nums) { if (heap.size() < k) { heap.insert(num); } else if ((findMax && num > heap.peek()) || (!findMax && num < heap.peek())) { heap.poll(); heap.insert(num); } } // 取出結果並反轉順序 int[] result = new int[heap.size()]; for (int i = result.length - 1; i >= 0; i--) { result[i] = heap.poll(); } return result; } } ``` - 接下來我們就來使用這個 Heap 類來實現堆的操作 ```java public class Main { public static void main(String[] args) { Heap maxHeap1 = new Heap(10, true); maxHeap1.insert(10); maxHeap1.insert(20); maxHeap1.insert(15); maxHeap1.insert(40); maxHeap1.insert(50); maxHeap1.insert(100); System.out.println("\n===最大堆==="); System.out.println(maxHeap1.poll()); // 100 System.out.println(maxHeap1.poll()); // 50 System.out.println(maxHeap1.poll()); // 40 System.out.println(maxHeap1.poll()); // 20 System.out.println(maxHeap1.poll()); // 15 System.out.println(maxHeap1.poll()); // 10 Heap minHeap1 = new Heap(10, false); minHeap1.insert(10); minHeap1.insert(20); minHeap1.insert(15); minHeap1.insert(40); minHeap1.insert(50); minHeap1.insert(100); System.out.println("\n===最小堆==="); System.out.println(minHeap1.poll()); // 10 System.out.println(minHeap1.poll()); // 15 System.out.println(minHeap1.poll()); // 20 System.out.println(minHeap1.poll()); // 40 System.out.println(minHeap1.poll()); // 50 System.out.println(minHeap1.poll()); // 100 int[] array = {4, 10, 3, 5, 1}; // 1. 測試自下而上建立最大堆 System.out.println("\n===自下而上建立最大堆==="); Heap maxHeap2 = new Heap(10, true); maxHeap2.buildHeapBottomUp(array); System.out.println("堆頂元素: " + maxHeap2.peek()); // 應該輸出 10 // 2. 測試自上而下建立最小堆 System.out.println("\n===自上而下建立最小堆==="); Heap minHeap2 = new Heap(10, false); minHeap2.buildHeapTopDown(array); System.out.println("堆頂元素: " + minHeap2.peek()); // 應該輸出 1 // 3. 測試堆排序 System.out.println("\n===堆排序==="); int[] sorted = minHeap2.heapSort(); System.out.print("排序結果: "); for (int num : sorted) { System.out.print(num + " "); // 應該輸出 1 3 4 5 10 } // 4. 測試找到資料流中的前 K 大元素 int[] nums = {4, 10, 3, 5, 1, 8, 7, 9, 2, 6}; // 測試找出前3大的數 System.out.println("\n\n===找出前3大的數==="); int[] topThreeLargest = Heap.findTopK(nums, 3, true); System.out.print("前3大的數: "); for (int num : topThreeLargest) { System.out.print(num + " "); // 預期輸出: 10 9 8 } System.out.println("\n\n===找出前3小的數==="); int[] topThreeSmallest = Heap.findTopK(nums, 3, false); System.out.print("前3小的數: "); for (int num : topThreeSmallest) { System.out.print(num + " "); // 預期輸出: 1 2 3 } // 測試邊界情況 System.out.println("\n\n===測試邊界情況==="); int[] singleNum = {1}; int[] result1 = Heap.findTopK(singleNum, 1, true); System.out.println("單一元素找最大: " + result1[0]); // 預期輸出: 1 int[] result2 = Heap.findTopK(nums, nums.length, true); System.out.print("k等於陣列長度: "); for (int num : result2) { System.out.print(num + " "); // 預期輸出: 全部元素,由大到小排序 } } } ``` - 在 maxHeap1 中,我們使用最大堆來實現,插入元素後,堆的結構如下 ```mermaid graph TD A[100]-->B[50] A-->C[40] B-->D[20] B-->E[15] C-->F[10] ``` - 在 minHeap1 中,我們使用最小堆來實現,插入元素後,堆的結構如下 ```mermaid graph TD A[10]-->B[15] A-->C[20] B-->D[40] B-->E[50] C-->F[100] ``` ### 針對 Heap 類別中的程式碼進行時間與空間複雜度分析 #### 基本操作複雜度 1. 查詢操作 - peek(): O(1) - size(): O(1) - isEmpty(): O(1) 2. 修改操作 - insert(): O(log n) - poll(): O(log n) - heapifyUp(): O(log n) - heapifyDown(): O(log n) #### 建堆方法複雜度 1. 自下而上建堆 (buildHeapBottomUp) - 時間複雜度: O(n) - 空間複雜度: O(n) 原因: - 只需處理非葉節點 - 從下往上處理,每層節點數減半 - 總操作次數為 n/4 + n/8 + n/16 + ... = n 2. 自上而下建堆 (buildHeapTopDown) - 時間複雜度: O(n log n) - 空間複雜度: O(n) 原因: - 每個元素都需要上浮操作 - n個元素,每個上浮最多log n層 #### 堆排序複雜度 heapSort() - 時間複雜度: O(n log n) - 空間複雜度: O(n) 原因: - 需要額外陣列儲存結果 - 每次取出堆頂元素需要O(log n) - 總共取出n個元素 #### 空間使用分析 1. 固定空間: - elements[]: O(n) - size: O(1) - isMaxHeap: O(1) 2. 臨時空間: - swap(): O(1) - heapSort(): O(n) ### 有機會再來實作其他堆的應用 - 合併 K 個有序陣列 - 實現中位數資料結構 - 堆的最佳實踐與優化