每日算法 — 使用java实现选择排序:简单直观的比较排序与稳定性分析

引言

选择排序(Selection Sort)是计算机科学中最基础、最直观的比较排序算法之一。它的核心思想非常朴素:在每一轮遍历中,从待排序区间里找出最小(或最大)的元素,将其放到已排序区间的末尾。尽管选择排序的时间复杂度并非最优,但它实现简单、空间复杂度极低,且排序过程中交换次数最少,这些特性使其在特定场景下仍具实用价值。本文将用Java完整实现选择排序,深入剖析其原理,并与其他基础排序算法进行性能对比。

算法原理

选择排序的算法步骤如下:

  1. 将数组分为已排序区间和未排序区间,初始时已排序区间为空。
  2. 从未排序区间中找到最小元素的索引。
  3. 将找到的最小元素与未排序区间的第一个元素交换位置。
  4. 已排序区间扩展一位,未排序区间缩小一位。
  5. 重复步骤2-4,直到未排序区间只剩一个元素。

对于一个长度为 n 的数组,选择排序需要进行 n-1 轮选择,每轮选择需要扫描剩余未排序元素。

核心Java实现

基础版本

/**
 * 选择排序基础实现
 * 时间复杂度:O(n²),空间复杂度:O(1)
 */
public class SelectionSort {

    /**
     * 对整型数组进行升序排序
     * @param arr 待排序数组
     */
    public static void sort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int n = arr.length;
        // 外层循环:控制已排序区间的右边界
        for (int i = 0; i < n - 1; i++) {
            // 假设当前位置i是最小值的位置
            int minIndex = i;
            // 内层循环:在未排序区间[i+1, n-1]中寻找真正的最小值
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            // 如果最小值不在位置i,则交换
            if (minIndex != i) {
                swap(arr, i, minIndex);
            }
        }
    }

    /**
     * 交换数组中两个位置的元素
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = {64, 25, 12, 22, 11, 90, 33, 77};
        System.out.println("排序前: " + arrayToString(arr));
        sort(arr);
        System.out.println("排序后: " + arrayToString(arr));
    }

    private static String arrayToString(int[] arr) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < arr.length; i++) {
            sb.append(arr[i]);
            if (i < arr.length - 1) sb.append(", ");
        }
        sb.append("]");
        return sb.toString();
    }
}

泛型版本(支持Comparable对象)

import java.util.Arrays;

/**
 * 选择排序泛型实现,支持任意Comparable类型
 */
public class GenericSelectionSort {

    /**
     * 对Comparable数组进行升序排序
     */
    public static <T extends Comparable<T>> void sort(T[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                // 使用compareTo进行元素比较
                if (arr[j].compareTo(arr[minIndex]) < 0) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                swap(arr, i, minIndex);
            }
        }
    }

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

    public static void main(String[] args) {
        String[] fruits = {"banana", "apple", "cherry", "date", "elderberry"};
        System.out.println("排序前: " + Arrays.toString(fruits));
        sort(fruits);
        System.out.println("排序后: " + Arrays.toString(fruits));

        Integer[] numbers = {5, 2, 8, 1, 9, 3};
        sort(numbers);
        System.out.println("数字排序: " + Arrays.toString(numbers));
    }
}

双向选择排序(每次找出最大和最小)

/**
 * 双向选择排序优化版
 * 每轮同时找出未排序区间的最大值和最小值
 * 交换次数从O(n)减少到约O(n/2)
 */
public class BidirectionalSelectionSort {

    public static void sort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        int left = 0;
        int right = arr.length - 1;

        while (left < right) {
            int minIndex = left;
            int maxIndex = right;

            // 在[left, right]区间内同时找最小和最大
            for (int i = left; i <= right; i++) {
                if (arr[i] < arr[minIndex]) {
                    minIndex = i;
                }
                if (arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
            }

            // 将最小值放到left位置
            swap(arr, left, minIndex);

            // 如果最大值恰好在left位置,交换后它到了minIndex位置
            if (maxIndex == left) {
                maxIndex = minIndex;
            }

            // 将最大值放到right位置
            swap(arr, right, maxIndex);

            left++;
            right--;
        }
    }

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

    public static void main(String[] args) {
        int[] arr = {64, 25, 12, 22, 11, 90, 33, 77, 5, 50};
        System.out.print("排序前: ");
        printArray(arr);
        sort(arr);
        System.out.print("排序后: ");
        printArray(arr);
    }

    private static void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

复杂度分析

时间复杂度

选择排序的时间复杂度在所有情况下都是 O(n²):

  • 最好情况:数组已经有序。选择排序仍然需要进行完整的比较操作,时间复杂度为 O(n²)。
  • 最坏情况:数组逆序。同样需要 O(n²) 的比较和交换。
  • 平均情况:O(n²)。

比较次数的精确计算:第一轮比较 n-1 次,第二轮 n-2 次,…,最后一轮 1 次。总比较次数为 (n-1) + (n-2) + ... + 1 = n(n-1)/2。

空间复杂度

选择排序是原地排序算法,仅需常数级别的额外空间用于临时变量交换,空间复杂度为 O(1)。

交换次数

选择排序的最大特点是交换次数极少,最多进行 n-1 次交换。相比冒泡排序在最坏情况下需要进行 O(n²) 次交换,选择排序在写操作代价较高的场景(如闪存、网络传输)具有优势。

稳定性

选择排序是不稳定排序。当将最小元素与前面位置交换时,可能改变相等元素的相对顺序。例如 [5a, 8, 5b, 2],第一次选择会将 2 与 5a 交换,得到 [2, 8, 5b, 5a],两个 5 的相对顺序被破坏。

性能对比测试

import java.util.Random;

/**
 * 选择排序与其他基础排序的性能对比
 */
public class SortComparison {

    public static void main(String[] args) {
        int[] sizes = {1000, 5000, 10000};
        Random rand = new Random(42);

        for (int size : sizes) {
            int[] arr1 = new int[size];
            int[] arr2 = new int[size];
            int[] arr3 = new int[size];

            for (int i = 0; i < size; i++) {
                int val = rand.nextInt(100000);
                arr1[i] = val;
                arr2[i] = val;
                arr3[i] = val;
            }

            System.out.println("\n数组大小: " + size);
            System.out.println("选择排序: " + benchmark(() -> selectionSort(arr1)) + " ms");
            System.out.println("冒泡排序: " + benchmark(() -> bubbleSort(arr2)) + " ms");
            System.out.println("插入排序: " + benchmark(() -> insertionSort(arr3)) + " ms");
        }
    }

    private static long benchmark(Runnable sortFunc) {
        long start = System.currentTimeMillis();
        sortFunc.run();
        return System.currentTimeMillis() - start;
    }

    private static void selectionSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            int minIdx = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIdx]) minIdx = j;
            }
            if (minIdx != i) {
                int tmp = arr[i]; arr[i] = arr[minIdx]; arr[minIdx] = tmp;
            }
        }
    }

    private static void bubbleSort(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1; i++) {
            boolean swapped = false;
            for (int j = 0; j < n - 1 - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp;
                    swapped = true;
                }
            }
            if (!swapped) break;
        }
    }

    private static void insertionSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int key = arr[i];
            int j = i - 1;
            while (j >= 0 && arr[j] > key) {
                arr[j + 1] = arr[j];
                j--;
            }
            arr[j + 1] = key;
        }
    }
}

适用场景与总结

选择排序适用于以下场景:
– 数据量较小:当 n < 1000 时,O(n²) 的算法性能可以接受。
– 写操作代价高:如闪存写入、网络I/O,选择排序仅需 O(n) 次交换。
– 需要最少交换:某些硬件或分布式系统中,减少交换次数意味着降低通信开销。
– 教学入门:算法思想极其直观,是理解排序算法的最佳起点。

选择排序虽然效率不高,但其简洁的实现和极低的交换次数使其在特定领域仍有一席之地。理解选择排序有助于建立对排序算法时间-空间权衡的直觉,为学习更高效的快速排序、归并排序和堆排序打下基础。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注