(d)选择排序

官官方信息技术老师·13 页·深入(追求细节与边界)·0 次浏览·38 天前
排序算法原地排序不稳定O(n²)

选择排序

从内层循环的每一步比较到交换,带你手推整个排序过程并算清复杂度

按 空格/→ 演示下一步

问一句
1 / 13 页

全部页面点击任意一页,跳回舞台从这页播放

排序算法原地排序不稳定O(n²)

选择排序

从内层循环的每一步比较到交换,带你手推整个排序过程并算清复杂度

1第 1 页 · 选择排序

核心构思:选出来放好

打扑克理牌时,你会先把手里最大的那张抽出来,放到最左边;再从剩下的牌里挑最大的,放到它右边。选择排序干的就是这件事:每一轮都「选出最大,放到该放的位置」。

每趟选极值
在未排序区间里扫一遍,找出最大(或最小)的那个元素
与边界交换
把它和未排序区间的边界位置交换,它就永久归位了
区间收缩
归位一个,未排序区间就少一个,重复 n-1 趟全部有序
打牌挑最大对应 →选择排序

每轮从手里挑出最大的牌,对应每趟从未排序区间选出极值

2第 2 页 · 核心构思:选出来放好

不变式:前缀已排序

上一页我们把最小的元素「选出来放好」。只选一次还不够——想象一条分界线从左往右爬:线左边永远是排好的,线右边待处理。这条线每走一格,排序就完成一格。

已排序前缀
分界线左边的部分,已就位且不再变动
无序后缀
分界线右边,每轮从中选最小值
不变式
任何时候前缀都≤后缀所有元素,这是正确性保证
边界移动
每轮交换后分界线右移一格,n-1轮后完成
爬格子填数字对应 →前缀扩张

填好的格子对应已排序前缀,空白格对应无序后缀

a1≤⋯≤ak⏟已排序前缀  ∣  ak+1 … an⏟无序后缀,均≥ak\underbrace{a_1\le\cdots\le a_k}_{\text{已排序前缀}}\;|\;\underbrace{a_{k+1}\,\dots\,a_n}_{\text{无序后缀,均}\ge a_k}
3第 3 页 · 不变式:前缀已排序

一趟完整流程分步走

一趟选择排序:扫描、比较、记录、交换,四步环环相扣。

1
扫描无序区
从未排序部分起点出发逐个查看元素,范围是本次要处理的所有候选
2
逐个比较
每个元素与当前已记录的最值比较,决定是否更新记录
3
记录最大值下标
只记下标不搬数据,扫描结束才知道最大元素在哪
4
交换到位
把最大值与无序区末尾元素交换,无序区缩小一格
4第 4 页 · 一趟完整流程分步走

完整实例演示

从上往下看:每趟选最小值换到前缀末尾,灰区(已排序前缀)逐趟变长。

图解渲染中…
t2:趟2最小就是3,原地不换,前缀仍扩张f1:n-1趟后,最后一个元素自然就位
5第 5 页 · 完整实例演示

框架实现

python

看到选择排序的双层循环骨架:外层管趟数,内层找最大,交换一次收尾。

代码高亮加载中…

外层 i 是未排序区左边界;内层只记下标不动手;一趟结束后才交换,把最大值放到前缀末尾——与不变式完全对应。

6第 6 页 · 框架实现

推敲:往哪放、放多少趟

前页框架已能跑通,但有两个细节值得推敲:每趟选出的最大值该放到区间的哪一端?整个过程要跑多少趟才算完?把这两个边界问题想透,代码才不会出错。

放到末尾
每趟最大值与未排序区间最后一个位置交换
区间收缩
每趟结束,未排序区间右端减一,有序后缀加一
跑 n-1 趟
剩最后一个元素时必然最大,无需再比
单元素免换
最大值已在末尾时,交换可跳过
排队报数出列对应 →选择排序趟数

每轮选出最高的站到队尾,队尾固定后不再参与,n 人只需 n-1 轮

第 i 趟扫描区间:[0,  n−i),i=1,2,…,n−1\text{第 } i \text{ 趟扫描区间:} [0,\; n-i), \quad i = 1, 2, \dots, n-1
7第 7 页 · 推敲:往哪放、放多少趟

selectMax() 实现

python

核心接口落地:返回无序区间中最大元素的位置,为交换做准备。

代码高亮加载中…

m 存下标而非元素值,比较用严格大于——这两点决定函数正确性与稳定性边界。

8第 8 页 · selectMax() 实现

自交换与稳定性推敲

都是每趟选一个元素归位,但“怎么放”藏着一个稳定性陷阱:直接交换会不会打乱相等元素的先后?

直接交换
  • 选中后与端位元素 swap,一步到位
  • 远距离交换,可能跨越相等元素
  • 不稳定:相等元素相对次序可能颠倒
插入搬运
  • 选中后逐个后移,腾位再放入
  • 相邻移动,只挤开中间元素
  • 稳定:相等元素次序得以保持
正确的理解是:选择排序天生的不稳定来自 swap 的远距离跳跃;改用插入式搬运可保稳定,但代价是移动次数变多。
9第 9 页 · 自交换与稳定性推敲

性能:比较多、移动少

上一页我们发现:不管数据多乱,每趟都得把剩余元素全扫一遍才能选出最值。这种「不看完不放心」的性格,让比较次数天生固定——而换来的是移动次数极少。

比较次数固定
每趟扫描剩余全部元素,总数恒为 n(n-1)/2,与数据是否有序无关
移动次数少
每趟至多交换一次,总交换次数 ≤ n-1,是交换类排序中最低档
交换的含义
这里的移动按「交换一次算 3 次赋值」计,依然远少于冒泡的频繁换位
认真看完全场再投票对应 →选择排序的比较

评委必须看完每个候选人,无论谁是热门;但最终只挪一次奖杯

C(n)=∑i=1n−1(n−i)=n(n−1)2,S(n)≤n−1C(n)=\sum_{i=1}^{n-1}(n-i)=\frac{n(n-1)}{2},\quad S(n)\le n-1
10第 10 页 · 性能:比较多、移动少

自测:一趟走几步

点击作答

对 n 个元素做选择排序(每趟从未排序区选出最值放好),以下关于趟数与比较次数的说法,正确的是?

11第 11 页 · 自测:一趟走几步

要点收束

  • ✓每趟从未排序区选出最值,换到前缀末尾,前缀不变式逐步扩张
  • ✓核心动作是比较,交换至多每趟一次,移动次数 O(n)
  • ✓选择排序不稳定:远距离交换会打乱相等元素的原有次序
  • ✓比较次数固定 n(n-1)/2,与输入是否有序无关
  • ✓掌握'选出来放好'的框架,堆排序是它的自然升级
延伸主题:堆排序如何把选最值降到 O(log n)为什么交换会破坏稳定性插入排序与选择排序的对比
12第 12 页 · 要点收束

课后思考

先自己想一会儿,再看参考答案——问题值得多想一会。

1为什么前缀已排序这个不变式,在交换发生后依然成立?

参考答案从不变式角度推敲:交换只发生在无序后缀内部,前缀元素不受影响。可再想想自交换那种边界情形,不变式是否依然完好。

2如果想让它稳定,你会怎么改?代价是什么?

参考答案往回对比插入排序的稳定思路:不跨越等值元素移动即可。可选链表接尾或整体后移腾位,但移动次数会明显增加,权衡就在这里。

3在交换代价很高的场景里,选择排序还占优吗?

参考答案每趟至多一次交换,总交换次数 O(n),这是它的看家本领。若比较便宜而写昂贵(如闪存),这个特性就值得认真考虑。

13第 13 页 · 课后思考