---
# System prepended metadata

title: 用Java學資料結構-堆Heap
tags: [資料結構, Java]

---

# 用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 個有序陣列
- 實現中位數資料結構

- 堆的最佳實踐與優化