首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

堆排序算法原理與 Java 實現全解析:從完全二叉樹到 O(n log n) 實戰

     編輯:Java 相關知識

前言:為什麼要掌握堆排序?

堆排序是原地排序、時間復雜度穩定 O(n log n)、無需額外空間(O(1))的經典算法。雖然不穩定,但其最壞情況性能優於快速排序,且是理解優先隊列、Top-K 問題、外部排序的基石。本文從數據結構原理推導到 Java 代碼實現,層層遞進攻克堆排序。

一、堆的數學定義與數組存儲映射

1. 完全二叉樹性質

  • 除最後一層外,每層節點數達最大值 (2h)。
  • 最後一層節點靠左排列,無空隙。

2. 堆的兩種形態

類型定義典型應用
最大堆父節點 ≥ 左右子節點;根節點為最大值堆排序升序、優先隊列取最大
最小堆父節點 ≤ 左右子節點;根節點為最小值Top-K 最大、優先隊列取最小

3. 數組下標映射公式(0-based 索引)


父節點索引:     parent(i) = (i - 1) / 2
左子節點索引:   left(i)   = 2*i + 1
右子節點索引:   right(i)  = 2*i + 2
最後一個非葉子節點:lastNonLeaf = (n - 2) / 2 = n/2 - 1

這種隱式數組表示法無需指針,緊湊高效,是堆排序「原地」的關鍵。

二、堆排序核心流程:兩階段法

階段 1:構建最大堆(Heapify / Build Heap)

從最後一個非葉子節點倒序到根節點,對每個節點調用 siftDown(下沉/調整),確保以該節點為根的子樹滿足堆性質。


// 自底向上構建堆:O(n)
for (int i = n/2 - 1; i >= 0; i--) {
    siftDown(arr, i, n);
}

時間復雜度證明:並非 O(n log n) 而是 O(n),因為大多數節點在樹底層,下沉高度小。具體推導見 CLRS 教材 6.3 節。

階段 2:反覆提取堆頂並重建

  1. 交換堆頂 (arr[0]) 與當前無序區末尾元素 (arr[i]),最大值歸位有序區。
  2. 無序區長度 -1,對新堆頂調用 siftDown 恢復堆性質。
  3. 重複直到無序區長度為 1。

for (int i = n - 1; i > 0; i--) {
    swap(arr, 0, i);           // 最大值移至有序區
    siftDown(arr, 0, i);       // 在縮小的無序區重建堆
}

三、核心調整算法:siftDown (下沉/堆化)


/**
 * 將索引 k 處的元素在長度為 n 的堆中下沉到合適位置
 * 前提:k 的左右子樹已是合法堆
 */
private static void siftDown(int[] arr, int k, int n) {
    while (true) {
        int left = 2*k + 1;
        int right = left + 1;
        int largest = k;
        
        // 找出父節點與兩子節點中的最大值索引
        if (left < n && arr[left] > arr[largest]) largest = left;
        if (right < n && arr[right] > arr[largest]) largest = right;
        
        // 父節點已是最大,堆性質滿足,結束
        if (largest == k) break;
        
        // 否則交換並繼續向下沉
        swap(arr, k, largest);
        k = largest;
    }
}

private static void swap(int[] arr, int i, int j) {
    int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
}

關鍵洞察:siftDown 只比較父節點與較大的子節點,每次下沉一層,最多高度 h = log₂n 次,單次調整 O(log n)。

四、完整 Java 實現(生產級代碼風格)


public class HeapSort {
    
    /** 公開入口:原地升序排序 */
    public static void sort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        int n = arr.length;
        
        // 1. 構建最大堆
        for (int i = n/2 - 1; i >= 0; i--) {
            siftDown(arr, i, n);
        }
        
        // 2. 反覆提取堆頂
        for (int i = n - 1; i > 0; i--) {
            swap(arr, 0, i);
            siftDown(arr, 0, i);
        }
    }
    
    /** 下沉調整:迭代版(避免遞歸棧溢出) */
    private static void siftDown(int[] arr, int k, int n) {
        while (true) {
            int left = 2*k + 1;
            int right = left + 1;
            int largest = k;
            
            if (left < n && arr[left] > arr[largest]) largest = left;
            if (right < n && arr[right] > arr[largest]) largest = right;
            if (largest == k) break;
            
            swap(arr, k, largest);
            k = largest;
        }
    }
    
    private static void swap(int[] arr, int i, int j) {
        int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
    }
    
    // 測試
    public static void main(String[] args) {
        int[] data = {4, 10, 3, 5, 1, 8, 7, 2, 9, 6};
        System.out.println("排序前:" + Arrays.toString(data));
        sort(data);
        System.out.println("排序後:" + Arrays.toString(data));
        // 輸出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    }
}

五、性能特徵總結表

指標數值說明
最好時間復雜度O(n log n)無論輸入分佈,流程固定
平均時間復雜度O(n log n)
最壞時間復雜度O(n log n)優於快排 O(n²) 最壞情況
空間復雜度O(1)原地排序,僅常數額外空間
穩定性不穩定相等元素相對位置可能改變
適應性無不利用已有序部分

六、實戰變體與優化

  1. 優先隊列:PriorityQueue 底層即最小堆,插入/取最小均 O(log n)。
  2. Top-K 最大元素:維護大小 K 的最小堆,遍歷數組 O(n log K),適合海量數據。
  3. 外部排序:歸併階段用最小堆優化多路歸併。
  4. Floyd 建堆優化:構建堆時從 n/2-1 遞減,已是標準最優做法。
  5. 底層優化:對小數組 (n < 32) 改用插入排序,減少函數調用開銷(JDK DualPivotQuicksort 採用類似策略)。

七、常見面試陷阱

  • ❌ 「堆排序是穩定的」——錯,交換堆頂與末尾元素會打破相等元素相對順序。
  • ❌ 「構建堆是 O(n log n)」——錯,正確是 O(n),證明關鍵在於葉子節點佔半數且無需下沉。
  • ✅ 「為什麼不直接用 Arrays.sort()?」——Arrays.sort() 基礎類型用雙軸快排(平均更快),對象用 TimSort(穩定、適應性強)。堆排序優勢在於最壞情況保證與原地特性。

總結

堆排序以「完全二叉樹隱式數組存儲」為基石,通過「構建堆 + 反覆提取堆頂」兩階段實現 O(n log n) 原地排序。掌握 siftDown 下沉邏輯與下標映射公式,是攻克優先隊列、Top-K、外部排序等進階話題的前提。建議手寫 2-3 遍迭代版 siftDown,體會「父節點與較大子節點比較、交換、繼續下沉」的循環不變式。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved