选择排序(Selection Sort)是一种简单直观的排序算法,其核心思想是通过不断寻找剩余元素中的最小值(或最大值)并将其放置在正确位置来完成排序。

选择排序是一种原地比较排序算法,其基本操作是在未排序序列中找到最小(或最大)元素,然后将其存放到序列的起始位置。接着,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。依此类推,直到所有元素均排序完毕。

冒泡排序 频繁交换元素不同,选择排序每轮只进行一次元素交换,这使得它在某些情况下比其更高效。

1、工作原理与步骤

选择排序的工作过程可以分为以下步骤:

  1. 从待排序序列的起始位置开始,假设该位置的元素为最小值

  2. 遍历剩余未排序的元素,寻找真正的最小值

  3. 将找到的最小值与序列起始位置的元素交换

  4. 移动起始位置到下一个未排序元素

  5. 重复步骤 1-4,直到所有元素都被排序

简而言之,选择排序就是不断 "选择" 剩余元素中的最值并将其放到正确位置的过程。

1.1 选择排序伪代码

function selectionSort(arr):
    n = length(arr)

    // 外层循环控制已排序部分的末尾位置
    for i from 0 to n-2:
        // 假设当前位置是最小值所在位置
        minIndex = i

        // 寻找剩余未排序元素中的最小值
        for j from i+1 to n-1:
            if arr[j] < arr[minIndex]:
                minIndex = j

        // 将找到的最小值与当前位置元素交换
        if minIndex != i:
            swap arr[i] and arr[minIndex]

    return arr

1.2 可视化理解

选择排序的过程可以通过动画直观展示:

选择排序动画演示
  • 整个序列分为已排序和未排序两个部分,有明显的分界

  • 每一轮都会在未排序部分高亮显示当前最小值的查找过程

  • 找到最小值后,会有一次交换操作将其放到已排序部分的末尾

  • 已排序部分通常用不同颜色标记,随着排序进行逐渐扩大

与冒泡排序相比,选择排序的交换操作明显更少,每轮只进行一次交换,这是其特点之一。

2、算法实现

2.1 Java 实现

public class SelectionSort {
    
    // 基本选择排序实现(升序)
    public static void selectionSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return; // 数组为空或只有一个元素,无需排序
        }
        
        int n = arr.length;
        
        // 外层循环:已排序元素的末尾位置
        for (int i = 0; i < n - 1; i++) {
            // 寻找未排序部分的最小值索引
            int minIndex = i;
            for (int j = i + 1; j < n; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j; // 更新最小值索引
                }
            }
            
            // 将找到的最小值与当前位置元素交换
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }
    
    // 优化版:同时寻找最小值和最大值,减少遍历次数
    public static void optimizedSelectionSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int left = 0;
        int right = arr.length - 1;
        
        while (left < right) {
            // 寻找最小值索引
            int minIndex = left;
            // 寻找最大值索引
            int maxIndex = right;
            
            // 确保初始值正确
            if (arr[minIndex] > arr[maxIndex]) {
                minIndex = right;
                maxIndex = left;
            }
            
            // 遍历寻找最小值和最大值
            for (int i = left + 1; i < right; i++) {
                if (arr[i] < arr[minIndex]) {
                    minIndex = i;
                } else if (arr[i] > arr[maxIndex]) {
                    maxIndex = i;
                }
            }
            
            // 交换最小值到左侧
            if (minIndex != left) {
                int temp = arr[left];
                arr[left] = arr[minIndex];
                arr[minIndex] = temp;
                
                // 如果最大值在左侧位置,需要更新其索引
                if (maxIndex == left) {
                    maxIndex = minIndex;
                }
            }
            
            // 交换最大值到右侧
            if (maxIndex != right) {
                int temp = arr[right];
                arr[right] = arr[maxIndex];
                arr[maxIndex] = temp;
            }
            
            // 缩小排序范围
            left++;
            right--;
        }
    }
}

2.2 Go 实现

package main

// 基本选择排序实现(升序)
func selectionSort(arr []int) {
    if arr == nil || len(arr) <= 1 {
        return // 数组为空或只有一个元素,无需排序
    }
    
    n := len(arr)
    
    // 外层循环:已排序元素的末尾位置
    for i := 0; i < n-1; i++ {
        // 寻找未排序部分的最小值索引
        minIndex := i
        for j := i + 1; j < n; j++ {
            if arr[j] < arr[minIndex] {
                minIndex = j // 更新最小值索引
            }
        }
        
        // 将找到的最小值与当前位置元素交换
        if minIndex != i {
            arr[i], arr[minIndex] = arr[minIndex], arr[i]
        }
    }
}

// 优化版:同时寻找最小值和最大值,减少遍历次数
func optimizedSelectionSort(arr []int) {
    if arr == nil || len(arr) <= 1 {
        return
    }
    
    left := 0
    right := len(arr) - 1
    
    while left < right {
        // 寻找最小值索引
        minIndex := left
        // 寻找最大值索引
        maxIndex := right
        
        // 确保初始值正确
        if arr[minIndex] > arr[maxIndex] {
            minIndex = right
            maxIndex = left
        }
        
        // 遍历寻找最小值和最大值
        for i := left + 1; i < right; i++ {
            if arr[i] < arr[minIndex] {
                minIndex = i
            } else if arr[i] > arr[maxIndex] {
                maxIndex = i
            }
        }
        
        // 交换最小值到左侧
        if minIndex != left {
            arr[left], arr[minIndex] = arr[minIndex], arr[left]
            
            // 如果最大值在左侧位置,需要更新其索引
            if maxIndex == left {
                maxIndex = minIndex
            }
        }
        
        // 交换最大值到右侧
        if maxIndex != right {
            arr[right], arr[maxIndex] = arr[maxIndex], arr[right]
        }
        
        // 缩小排序范围
        left++
        right--
    }
}

3、算法优化

标准选择排序可以通过以下方式进行优化:

  1. 双向选择排序:每轮同时寻找最小值和最大值,分别放到序列的两端,减少一半的遍历次数

  2. 标记交换位置:只有当找到的最小值不在当前位置时才进行交换,避免不必要的交换操作

  3. 对于近乎有序的数据:可以记录上次最小值的位置,作为下次查找的起点

这些优化虽然不能改变选择排序的时间复杂度级别,但能在一定程度上提高实际运行效率。

4、复杂度分析

  • 时间复杂度

    • 最坏情况:O (n²)

    • 最好情况:O (n²)

    • 平均情况:O (n²)

选择排序的时间复杂度不受输入数据的有序程度影响,因为无论数据初始状态如何,它都需要完成所有元素的比较。

  • 空间复杂度:O (1),属于原地排序算法,只需要一个临时变量用于交换元素

  • 稳定性:选择排序是不稳定的排序算法。例如,对于序列 [2, 2, 1],第一个 2 会与 1 交换,导致两个 2 的相对顺序发生改变。

5、与其他排序算法的比较

算法

平均时间复杂度

空间复杂度

稳定性

每轮交换次数

选择排序

O(n²)

O(1)

不稳定

1 次

冒泡排序

O(n²)

O(1)

稳定

多次

插入排序

O(n²)

O(1)

稳定

多次

选择排序与同为 O (n²) 时间复杂度的冒泡排序和插入排序相比:

  • 选择排序的交换次数更少(每轮最多一次)

  • 选择排序的比较次数与冒泡排序相当,但多于插入排序

  • 选择排序不稳定,而冒泡和插入排序稳定

  • 选择排序的性能受数据初始状态影响较小

6、适用场景

选择排序适用于以下场景:

  1. 数据量较小的情况:对于小规模数据,O (n²) 的时间复杂度可以接受

  2. 硬件资源受限的环境:选择排序空间复杂度为 O (1),内存占用少

  3. 交换操作成本较高的场景:由于选择排序交换次数少,适合元素交换成本高的情况

在实际开发中,对于大规模数据,通常会选择快速排序、归并排序等更高效的算法。