目录

你有没有这样整理过书架?

扫一眼所有书,找到最矮的那本放到最左边,

再从剩下的里面找最矮的放第二位… 一趟一趟地,每轮只做一件事,在未排序的部分里选一个最小值。

这个过程就是选择排序(Selection Sort) 。

排序的过程中,每轮都从未排序区选一个最小元素,交换到已排序区的末尾。

思路简单到不需要任何花哨技巧,但它的特性(不稳定、比较次数固定)让它在面试和理论分析中经常被拿来当对比基准。


它是什么

核心思想 把数组分成”已排序区”和”未排序区”。

初始时已排序区为空。

每轮在未排序区里找最小元素,直接交换到已排序区的末尾。

重复 n-1 轮后全局有序。

“每次选一个最小的放过去,不关心中间过程,只关心结果。” , 这就是选择排序的全部逻辑。

伪代码

// 仅为示意
for i = 0 to n-2:
    minIdx = i
    for j = i+1 to n-1:
        if arr[j] < arr[minIdx]:
            minIdx = j
    if minIdx != i:
        swap(arr[i], arr[minIdx])

核心就两层循环。

外层控制”当前要填哪个位置”。

内层扫描剩余部分找最小值。

下面用流程图拆解。

工作原理流程图

先看整体骨架(对应伪代码的外层循环):

flowchart LR
    A([开始]) --> B[i = 0]
    B --> C{ i < n-1 ? }
    C -->|否| Z([结束])
    C -->|是| D[在 i..n-1 中找最小值 minIdx]
    D --> E{"arr[minIdx] != arr[i] ?"}
    E -->|是| F["交换 arr[i] 与 arr[minIdx]"]
    E -->|否| G[跳过,无需交换]
    F --> H[++i]
    G --> H
    H --> C

再看内层”找最小值”的细节:

flowchart LR
    A([开始找最小]) --> B[minIdx = i, j = i+1]
    B --> C{ j < n ? }
    C -->|否| D([返回 minIdx])
    C -->|是| E{"arr[j] < arr[minIdx] ?"}
    E -->|是| F[minIdx = j]
    E -->|否| G[不变]
    F --> H[++j]
    G --> H
    H --> C

逐趟演示

用 [5, 3, 8, 4, 2] 跑一遍,看每轮发生了什么:

初始数组: [5, 3, 8, 4, 2]

第 1 趟 (选最小放位置 0): [2] [3,8,4,5]
       在 [5,3,8,4,2] 中最小值是 2,与位置 0 的 5 交换

第 2 趟 (选最小放位置 1): [2,3] [8,4,5]
       在 [3,8,4,5] 中最小值是 3,已经在正确位置

第 3 趟 (选最小放位置 2): [2,3,4] [8,5]
       在 [8,4,5] 中最小值是 4,与位置 2 的 8 交换

第 4 趟 (选最小放位置 3): [2,3,4,5] [8]
       在 [8,5] 中最小值是 5,与位置 3 的 8 交换

最终结果: [2, 3, 4, 5, 8]

注意观察:每轮只发生一次交换(或零次),这是选择排序和冒泡排序最大的行为差异。冒泡可能一轮交换多次,选择每轮最多一次。


能解决什么问题

适用场景

场景说明
教学入门逻辑最直观,代码量最少
小规模数据(n < 50)常数因子小,简单场景可接受
写操作昂贵(如 EEPROM)每轮最多 1 次交换,写入次数最少之一
数据量极大不推荐,O(n²) 比较太慢
几乎有序不推荐,比较次数固定,不如插入排序

局限性

局限说明
时间 O(n²)数据量大时太慢,n=10000 就要约 5000 万次比较
不稳定相等元素的相对顺序会被改变(下面会演示)
比较次数固定不管输入是否有序,都是 n(n-1)/2 次比较,无法提前退出
在链表上实现不如数组高效每轮寻找最小值需从头遍历,无法利用缓存局部性,且指针操作较数组下标更繁琐;但并非”不能实现”

稳定性问题

程序验证

以下数据展示不稳定性的根本原因。

选择排序是不稳定排序。来看反例:

原始: [2a, 2b, 1]

第 1 轮:在 [2a, 2b, 1] 中找最小 → 1(位置 2),与位置 0 的 2a 交换 → [1, 2b, 2a]

两个 2 的相对顺序从 (2a, 2b) 变成了 (2b, 2a),稳定性被破坏。

跨越相等元素的交换直接打乱了原始相对顺序,这是选择排序无法保证稳定性的根本原因。


C++ 实现与复杂度分析

通用实现 可直接复用的代码:

template <typename T>
void selectionSort(std::vector<T>& arr) {
    int n = arr.size();
    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)
            std::swap(arr[i], arr[minIdx]);
    }
}

支持任意可比较类型,原地排序,空间 O(1)。

时间复杂度怎么算

基本操作是比较(arr[j] < arr[minIdx])。

外层跑 n-1 轮。

第 i 轮(i 从 0 开始)的内层比较次数 = (n-1) - (i+1) + 1 = n-i-1 次。

总比较次数 = 第 0 轮 + 第 1 轮 + … + 第 n-2 轮

= (n-1) + (n-2) + … + 1

= 等差数列求和 = n(n-1)/2

去系数留主项:O(n²)

关键点:这个次数是固定的,不管输入是否已经有序。

程序也验证了这一点:

输入比较交换
已有序 [1,2,3,4,5]100
完全逆序 [5,4,3,2,1]102

n=5 时 n(n-1)/2 = 10,两种情况比较次数完全一样。区别只在交换次数。

情况比较次数交换次数时间复杂度
最好n(n-1)/20O(n²)
最坏n(n-1)/2n-1O(n²)
平均n(n-1)/2≈ n/2O(n²)

最坏交换次数 n-1 当且仅当每轮最小值都不在当前位置时才达到;已有序时交换次数为 0。

三种情况时间复杂度全是 O(n²),这是选择排序的一个显著特征。

空间复杂度怎么算

只用了 i、j、minIdx 和交换临时变量这几个标量,与数据规模 n 无关。

所以空间复杂度 O(1),原地排序。


总结

  • 是什么:每轮从未排序区选最小元素交换到已排序区末尾,重复 n-1 轮。
  • 解决什么 / 用在哪:把无序变有序;适合教学、小规模数据、交换成本高的场景。
  • 局限:O(n²) 太慢、不稳定、比较次数无法减少(即使已有序也要跑满)。
  • 复杂度怎么算:基本操作是比较 → 内层循环次数构成等差数列 → 总计 n(n-1)/2 → O(n²);空间只看标量变量 → O(1)。三种情况时间复杂度相同是它的一大特点。
  • 进阶联想:把”线性扫描找最值”换成二叉堆加速,就是堆排序,同样的选最值思想,换套数据结构,复杂度从 O(n²) 降到 O(n log n)。