1. 引言
上篇博客我们聊了插入排序,那种像打扑克牌一样,把新牌一张张插到合适位置的感觉,相信大家还记忆犹新。今天,我们换个思路,来聊聊另一种同样经典的基础排序算法------选择排序。
如果说插入排序是"边摸牌边整理",那选择排序更像是"挑最顺眼的先拿走"。它的思路非常直观:每一轮都从剩下的元素里挑出最小的那个,放到前面去。就像我们在宿舍里整理书架,每次都从乱堆里挑出最薄的一本书放到最左边,一本一本排好。
2. 选择排序的基本思想
选择排序的核心思想可以用一句话概括:每一轮从未排序区间中选出最小值,放到已排序区间的末尾。
具体来说,假设我们有一个长度为 n 的数组,排序过程分为 n-1 轮:
- 第 1 轮:在全部 n 个元素中找出最小值,与第 1 个位置交换;
- 第 2 轮:在剩下的 n-1 个元素中找出最小值,与第 2 个位置交换;
- 第 3 轮:在剩下的 n-2 个元素中找出最小值,与第 3 个位置交换;
- ......
- 第 n-1 轮:在剩下的 2 个元素中找出最小值,与第 n-1 个位置交换。
经过 n-1 轮之后,最后一个元素自然就是最大值,排序完成。
2.1 一个直观的比喻
想象你在整理一摞试卷,要从乱序的卷子里按分数从低到高排好:
- 先扫一遍所有试卷,找出分数最低的那张,放到最上面;
- 再从剩下的试卷里找出分数最低的,放到第二张的位置;
- 重复这个过程,直到所有试卷排好。
这个过程就是选择排序的生动写照------每次只做一件事:找到当前最小的,放到它该去的位置。
2.2 动图

3. 图解选择排序过程
为了让大家看得更清楚,我们用一个具体例子来走一遍完整流程。
假设数组为:[5, 3, 8, 1, 2],长度为 5,需要 4 轮排序。
第 1 轮 :在 [5, 3, 8, 1, 2] 中找最小值 1,与第 1 个元素 5 交换:
[1, 3, 8, 5, 2]
第 2 轮 :在 [3, 8, 5, 2] 中找最小值 2,与第 2 个元素 3 交换:
[1, 2, 8, 5, 3]
第 3 轮 :在 [8, 5, 3] 中找最小值 3,与第 3 个元素 8 交换:
[1, 2, 3, 5, 8]
第 4 轮 :在 [5, 8] 中找最小值 5,与第 4 个元素 5 交换(位置不变):
[1, 2, 3, 5, 8]
排序完成!可以看到,每一轮结束后,前面就多了一个已经排好序的元素。
下面是整个过程的流程图:
#mermaid-svg-IrhKtRmjuuY1qKnJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IrhKtRmjuuY1qKnJ .error-icon{fill:#552222;}#mermaid-svg-IrhKtRmjuuY1qKnJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IrhKtRmjuuY1qKnJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .marker.cross{stroke:#333333;}#mermaid-svg-IrhKtRmjuuY1qKnJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IrhKtRmjuuY1qKnJ p{margin:0;}#mermaid-svg-IrhKtRmjuuY1qKnJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .cluster-label text{fill:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .cluster-label span{color:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .cluster-label span p{background-color:transparent;}#mermaid-svg-IrhKtRmjuuY1qKnJ .label text,#mermaid-svg-IrhKtRmjuuY1qKnJ span{fill:#333;color:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .node rect,#mermaid-svg-IrhKtRmjuuY1qKnJ .node circle,#mermaid-svg-IrhKtRmjuuY1qKnJ .node ellipse,#mermaid-svg-IrhKtRmjuuY1qKnJ .node polygon,#mermaid-svg-IrhKtRmjuuY1qKnJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .rough-node .label text,#mermaid-svg-IrhKtRmjuuY1qKnJ .node .label text,#mermaid-svg-IrhKtRmjuuY1qKnJ .image-shape .label,#mermaid-svg-IrhKtRmjuuY1qKnJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-IrhKtRmjuuY1qKnJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .rough-node .label,#mermaid-svg-IrhKtRmjuuY1qKnJ .node .label,#mermaid-svg-IrhKtRmjuuY1qKnJ .image-shape .label,#mermaid-svg-IrhKtRmjuuY1qKnJ .icon-shape .label{text-align:center;}#mermaid-svg-IrhKtRmjuuY1qKnJ .node.clickable{cursor:pointer;}#mermaid-svg-IrhKtRmjuuY1qKnJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .arrowheadPath{fill:#333333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IrhKtRmjuuY1qKnJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IrhKtRmjuuY1qKnJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IrhKtRmjuuY1qKnJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IrhKtRmjuuY1qKnJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .cluster text{fill:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ .cluster span{color:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IrhKtRmjuuY1qKnJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IrhKtRmjuuY1qKnJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-IrhKtRmjuuY1qKnJ .icon-shape,#mermaid-svg-IrhKtRmjuuY1qKnJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IrhKtRmjuuY1qKnJ .icon-shape p,#mermaid-svg-IrhKtRmjuuY1qKnJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IrhKtRmjuuY1qKnJ .icon-shape .label rect,#mermaid-svg-IrhKtRmjuuY1qKnJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IrhKtRmjuuY1qKnJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IrhKtRmjuuY1qKnJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IrhKtRmjuuY1qKnJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
否
输入待排序数组
i = 0
在 i 到 n-1 区间找最小值下标 minIndex
minIndex 是否等于 i?
交换 arri 与 arrminIndex
无需交换
i = i + 1
i < n-1?
输出排序后数组
4. 代码实现
c
void Swap(int* x, int* y)//交换两数据位置
{
int temp = *x;
*x = *y;
*y = temp;
}
void SelectSort(int* arr, int n)//简单选择排序
{
if (arr == NULL || n <= 1) return;
//1.核心思想是在未排序数据中选出最大/小的值,并与待交换数据交换位置,与插入排序的差别之一就是,插入排序比较过程中需要将元素后移,直到遇到合适位置插入
// 选择排序是先比较完,然后将单趟最值与某值交换位置,因此同样需要区分已排序和未排序区域
for (int i = 0; i < n - 1; i++)//同样的,防止越界
{
int min = i;//2.定义当前最小值的位置
for (int j = i+1; j < n; j++)//3.寻找未排序数据中的最小值
{
if (arr[j] < arr[min])
{
min = j;//4.如果有,就定义新的最小值的位置
}
}
//5.比完以后得到本次最小值的位置,与原本的最小值交换位置
if(min != i){
Swap(&arr[min], &arr[i]);
}
}
}
4.3 代码要点总结
- 外层循环
i表示已排序区间的末尾位置,从 0 到 n-2; - 内层循环
j负责在未排序区间中扫描,记录最小值下标min; - 每一轮最多交换一次,这是选择排序与冒泡排序的重要区别(冒泡可能一轮交换多次)。
5. 复杂度分析
5.1 时间复杂度
- 最好情况:O(n²)。即使数组已经有序,内层循环依然要完整扫描未排序区间,无法提前终止。
- 最坏情况:O(n²)。无论数据怎么分布,比较次数都是固定的。
- 平均情况:O(n²)。
具体来说,比较次数恒为 n(n-1)/2,与数据初始顺序无关;交换次数最多为 n-1 次(每轮最多一次)。
5.2 空间复杂度
- 空间复杂度 :O(1),只需要一个临时变量用于交换,属于原地排序算法。
5.3 稳定性分析
选择排序是不稳定的 。举个例子:数组 [5a, 5b, 1](两个 5 用 a、b 区分),第一轮找到最小值 1 与第一个 5a 交换,得到 [1, 5b, 5a],此时两个 5 的相对顺序发生了改变。这一点在面试中经常被问到,大家要特别注意。
有同学会问:如果把内层循环的比较条件从 < 改成 <=,能否让选择排序变稳定?
答案是不能 。选择排序的不稳定性根源在于交换 操作,而不是比较条件。即使你用 <= 让内层循环在遇到相等元素时也更新 min 下标,也只是改变了"选哪个相等元素"的策略,但交换本身依然可能破坏相对顺序。
以数组 [5, 6, 5, 0, 2, 0, 3] 为例,给相同元素编号:[5a, 6, 5b, 0a, 2, 0b, 3]:
- 第 1 轮 :找最小值
0a,与第 1 个元素5a交换,得到[0a, 6, 5b, 5a, 2, 0b, 3]。此时5a被换到了5b的后面 ,两个 5 的相对顺序已被破坏;而这一轮0a和0b的相对顺序反而保住了。 - 第 2 轮 :在
[6, 5b, 5a, 2, 0b, 3]中找最小值0b,与第 2 个元素6交换,得到[0a, 0b, 5b, 5a, 2, 6, 3]。这一轮0b又被换到了0a的后面,两个 0 的相对顺序也被破坏了。
可以看到:即使能保证某一组相同元素的相对顺序,也无法同时保证所有组的相对顺序。因为每一轮交换都会把"当前最小值"从后面直接"跨越"到前面,跨越过程中会越过所有其他元素------包括与它值相同的元素。
- 用
<时,相等元素不会被选中,相对顺序靠"运气"; - 用
<=时,相等元素会被选中(选最后一个),虽然可能保住某一组的顺序,但其他组的顺序照样会被交换破坏。
所以,只要选择排序采用"交换"来实现,它就注定不稳定。要让选择排序变稳定,必须改用"插入"的方式(把最小值前面的元素整体后移,再把最小值放进去),而不是交换------但那样它就不再是经典的选择排序了。
用循环不变量再证一遍
上面的例子已经足够直观,但如果你想让论证更严谨,可以用循环不变量 来证明:无论比较条件用 < 还是 <=,选择排序都不可能是稳定的。
先回顾一下选择排序外层循环的不变量:
循环不变量 :在第
i轮开始前(即i次交换完成后),数组的前i个位置已经排好序,且它们恰好是原数组中最小的i个元素;剩余n-i个元素仍在未排序区间[i, n-1]中。
这个不变量在初始化(i=0 时前 0 个元素自然有序)、保持(每轮把未排序区间的最小值放到 i 位置)、终止(i=n-1 时前 n-1 个元素有序,最后一个自然最大)三个阶段都成立,所以它保证了排序的正确性。
但注意:这个不变量只约束了"哪些值"被放到了前面,完全没有约束"相同值的相对顺序" 。它只保证前 i 个位置是"最小的 i 个元素",至于这些相等元素谁先谁后,不变量一概不管。
现在我们来证明不稳定。考虑任意一轮 i,设未排序区间 [i, n-1] 中存在两个相等的元素 x 和 y,且 x 在 y 的前面(x 的下标更小)。这一轮要选出的最小值记为 m,分两种情况:
- 情况一:
m就是x或y本身 。假设选中的是y(即y是未排序区间的最小值,且x与它相等)。交换后y被放到位置i,而x留在原地。由于x原本在y前面,交换后y跑到了x的前面,两者相对顺序被破坏。 - 情况二:
m是另一个更小的元素 。此时m会被交换到位置i,而x、y的相对位置在这一轮保持不变。但注意:这一轮没有破坏它们,不代表后面不会破坏 。在后续某一轮i',当x或y成为未排序区间的最小值被选中并交换到前面时,就会发生情况一,破坏它们的相对顺序。
换句话说:只要存在两个相等的元素,且它们不是同一轮被同时选中(选择排序每轮只选一个最小值,永远不可能同时选中两个),那么迟早有一轮会把其中一个"跨越"到另一个的前面。这正是选择排序"交换"实现的本质缺陷。
所以结论是:循环不变量只能证明选择排序"排对了",却无法证明它"排得稳"。稳定性需要的是"相等元素相对顺序不变"这一条额外约束,而选择排序的交换操作天然无法保证这一点。这也解释了为什么面试中"稳定性"和"正确性"是两个独立的问题------一个算法可以正确但不稳定。
6. 选择排序 vs 插入排序
上篇我们学了插入排序,这里把两者放在一起对比,方便大家记忆:
| 对比维度 | 选择排序 | 插入排序 |
|---|---|---|
| 基本思路 | 每轮选最小值放前面 | 每轮把新元素插入到合适位置 |
| 最好时间复杂度 | O(n²) | O(n)(已有序时) |
| 最坏时间复杂度 | O(n²) | O(n²) |
| 交换次数 | 最多 n-1 次 | 最多 n(n-1)/2 次 |
| 稳定性 | 不稳定 | 稳定 |
| 适用场景 | 交换成本高、数据量小 | 数据基本有序、数据量小 |
小贴士:如果交换两个元素的代价很大(比如元素是复杂对象),选择排序的交换次数少,反而可能比插入排序更合适;但如果数据基本有序,插入排序凭借提前终止的优势会快很多。
7. 常见面试考点
选择排序虽然简单,但面试官经常从它入手考察基础功底:
- 手写代码 :要求在白板上写出完整实现,注意边界条件
i < n-1和j < n。 - 稳定性判断:能举出反例说明它不稳定。
- 复杂度推导 :能说出比较次数恒为
n(n-1)/2。 - 优化思路:能否想到"同时找最大值和最小值"的优化版本(每轮找最大和最小,可减少一半轮次)。
- 与其它排序对比:能清晰说出选择、插入、冒泡三者的区别。
8. 总结
选择排序是算法入门路上绕不开的一块基石。它的思想朴素而优雅------每次只做一件最简单的事,重复 n-1 次,就能完成整个排序。
回顾一下今天的关键点:
- 核心思想:每轮从未排序区间选最小值,放到已排序区间末尾;
- 时间复杂度:无论好坏都是 O(n²);
- 空间复杂度:O(1),原地排序;
- 稳定性:不稳定;
- 与插入排序相比,交换次数更少,但无法利用数据的有序性。
下篇博客,我打算写冒泡排序,把这三个 O(n²) 的基础排序放在一起做个对比