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

Java 選擇排序演算法完整解析:原理、實作與優化變體

     編輯:Java 相關知識

前言

選擇排序是最直觀的排序演算法之一:每輪從未排序區間選出最小(或最大)元素,與未排序區首位元素交換。雖時間複雜度 O(n²) 不適合大量資料,但交換次數極少 (O(n))、空間複雜度 O(1),是教學與小規模資料的經典選擇。本文詳細解析基本版、優化版、樹形選擇排序三種變體。

演算法核心邏輯

  1. 外層迴圈 i = 0 ~ n-2,代表當前待確定的位置
  2. 內層迴圈 j = i+1 ~ n-1,在剩餘元素中尋找最小值索引 minIdx
  3. 若 minIdx != i,交換 arr[i] 與 arr[minIdx]
  4. 重複直到 i = n-2,最後一個元素自然就位

基本版實作 (Generic)


public class SelectionSort {

    /**
     * 選擇排序:升序,支援任何實作 Comparable 的型別
     * @param  元素型別
     * @param arr 待排序陣列(原地排序)
     */
    public static <T extends Comparable<T>> void selectionSort(T[] arr) {
        if (arr == null || arr.length <= 1) return;
        
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            int minIdx = i;
            // 尋找 [i+1, n-1] 區間的最小值索引
            for (int j = i + 1; j < n; j++) {
                if (arr[j].compareTo(arr[minIdx]) < 0) {
                    minIdx = j;
                }
            }
            // 僅在找到更小值時交換
            if (minIdx != i) {
                T temp = arr[i];
                arr[i] = arr[minIdx];
                arr[minIdx] = temp;
            }
        }
    }

    // 測試用 Date 類別(實作 Comparable)
    static class Date implements Comparable<Date> {
        int year, month, day;
        Date(int y, int m, int d) { year = y; month = m; day = d; }
        
        @Override public int compareTo(Date o) {
            if (year != o.year) return Integer.compare(year, o.year);
            if (month != o.month) return Integer.compare(month, o.month);
            return Integer.compare(day, o.day);
        }
        @Override public String toString() { return year + "-" + month + "-" + day; }
    }

    public static void main(String[] args) {
        Date[] dates = {
            new Date(2023, 5, 20),
            new Date(2022, 12, 1),
            new Date(2023, 1, 15),
            new Date(2021, 8, 30)
        };
        
        System.out.println("排序前: " + Arrays.toString(dates));
        selectionSort(dates);
        System.out.println("排序後: " + Arrays.toString(dates));
    }
}

基本版效能分析

指標數值說明
時間複雜度O(n²)最好/最差/平均皆同,約 n²/2 次比較
空間複雜度O(1)原地排序,僅需常數額外空間
交換次數0 ~ n-1最少交換排序法之一
穩定性不穩定相等元素相對順序可能改變
適應性無輸入有序度不影響比較次數

優化版:減少不必要的比較(雙向選擇排序)

每輪同時找出最小值與最大值,放置於兩端,輪數減半:


public static <T extends Comparable<T>> void bidirectionalSelectionSort(T[] arr) {
    if (arr == null || arr.length <= 1) return;
    
    int left = 0, right = arr.length - 1;
    while (left < right) {
        int minIdx = left, maxIdx = right;
        
        for (int i = left; i <= right; i++) {
            if (arr[i].compareTo(arr[minIdx]) < 0) minIdx = i;
            if (arr[i].compareTo(arr[maxIdx]) > 0) maxIdx = i;
        }
        
        // 先處理最小值
        if (minIdx != left) swap(arr, left, minIdx);
        // 若最大值原本在 left,交換後已移至 minIdx
        if (maxIdx == left) maxIdx = minIdx;
        
        // 再處理最大值
        if (maxIdx != right) swap(arr, right, maxIdx);
        
        left++;
        right--;
    }
}

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

進階變體:樹形選擇排序 - 以空間換時間

建立完全二元樹(錦標賽樹),葉節點為元素,內部節點儲存子樹最小值。每次取根節點(最小值),將該葉節點設為無窮大,向上更新路徑。


public static void treeSelectionSort(int[] arr) {
    if (arr == null || arr.length <= 1) return;
    
    int n = arr.length;
    // 樹大小:2*n - 1 (索引 0 為根)
    int[] tree = new int[2 * n - 1];
    
    // 1. 建樹:葉節點填入資料 (索引 n-1 ~ 2n-2)
    for (int i = 0; i < n; i++) {
        tree[n - 1 + i] = arr[i];
    }
    // 內部節點自底向上計算最小值
    for (int i = n - 2; i >= 0; i--) {
        tree[i] = Math.min(tree[2*i + 1], tree[2*i + 2]);
    }
    
    // 2. 依序取出最小值
    for (int k = 0; k < n; k++) {
        arr[k] = tree[0]; // 根節點即最小值
        
        // 找到該最小值對應的葉節點並設為 MAX_VALUE
        int leafIdx = findLeaf(tree, 0, arr[k]);
        tree[leafIdx] = Integer.MAX_VALUE;
        
        // 向上更新祖先節點
        updateAncestors(tree, leafIdx);
    }
}

private static int findLeaf(int[] tree, int node, int target) {
    if (node >= tree.length) return -1;
    if (2*node + 1 >= tree.length) { // 葉節點
        return tree[node] == target ? node : -1;
    }
    int left = findLeaf(tree, 2*node + 1, target);
    if (left != -1) return left;
    return findLeaf(tree, 2*node + 2, target);
}

private static void updateAncestors(int[] tree, int idx) {
    while (idx > 0) {
        int parent = (idx - 1) / 2;
        int newVal = Math.min(tree[2*parent + 1], tree[2*parent + 2]);
        if (tree[parent] == newVal) break; // 未改變可提早結束
        tree[parent] = newVal;
        idx = parent;
    }
}

三種變體比較

演算法時間複雜度空間複雜度比較次數交換/移動次數適用場景
基本選擇排序 O(n²) O(1) n(n-1)/2 0 ~ n-1 教學、小陣列、記憶體極限環境
雙向選擇排序 O(n²) O(1) ~n²/2 (輪數減半) 0 ~ n-1 略優於基本版,實務仍不如插入排序
樹形選擇排序 O(n log n) O(n) n log n n log n (樹更新) 外部排序、需穩定 O(n log n) 且空間允許

實務建議

  • 小陣列 (n < 50):插入排序通常比選擇排序快(適應性佳)。
  • 一般用途:直接用 Arrays.sort() (Dual-Pivot QuickSort / TimSort),O(n log n) 且高度優化。
  • 嵌入式/即時系統:選擇排序的確定性 O(n²) 與極少交換適合資源受限環境。
  • 外部排序:樹形選擇排序(替換選擇)是建立初始有序段的經典方法。

總結

選擇排序雖非高效能排序主流,但其原地、極少交換、邏輯清晰的特性使其在教學、嵌入式、外部排序啟動階段仍具價值。理解基本版、雙向優化與樹形變體的時空權衡,有助於建立演算法設計的權衡思維。

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