(b3)选取:通用算法

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·39 天前
选择算法快速选择复杂度分析

选取:通用算法

看完你能讲清快速选择如何在平均 O(n) 时间内找到第 k 大元素

按 空格/→ 演示下一步

问一句
1 / 14 页

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

选择算法快速选择复杂度分析

选取:通用算法

看完你能讲清快速选择如何在平均 O(n) 时间内找到第 k 大元素

1第 1 页 · 选取:通用算法

什么是选取问题

上一页我们看到:选第 k 大不必排序全部元素。就像找班里第 5 高的人,不需要给所有人排好队——你只关心那一个人是谁。这类「只挑不排」的问题,就叫选取问题。

第 k 众数问题
给定无序数组,找出第 k 大(或第 k 小)的那个元素
与排序的关系
排序后取第 k 位固然可行,但代价 O(n log n)
全排序是多余的
我们只需要一个位置上的答案,其余元素排好是白费功夫
目标下界
至少要看遍每个元素,所以 O(n) 才是合理的目标
找班级第 5 高对应 →第 k 选取

「排队取第 5 名」对应排序法,「直接挑出那一个」对应选取算法

2第 2 页 · 什么是选取问题

quickSelect 的算法流程

quickSelect 借快排划分,每轮只留一侧,候选区间逐步收缩。

1
选基准
从当前区间挑一个元素作为 pivot,划分的参照点
2
一趟划分
比 pivot 小的放左、大的放右,pivot 落定最终位置 k'
3
判断位置
k'==目标下标则直接返回;否则确定目标在哪一侧
4
单侧递归
只递归包含目标的那一侧,另一侧整个丢弃,区间减半
3第 3 页 · quickSelect 的算法流程

quickSelect 代码实现

python

quickSelect 的完整 Python 实现,重点看递归为何只走一个分支。

代码高亮加载中…

每次划分后比较 k 与基准下标 p:相等即答案,否则只递归目标所在的一侧——这就是它比快排快的原因,均摊 O(n)。

4第 4 页 · quickSelect 代码实现

quickSelect 的性能

上一页我们写出了 quickSelect 的代码——它不递归两边,只走一边,看起来比快排省事。但省事不等于稳定:走哪边、划分多均衡,全凭运气。

平均 O(n)
每轮期望丢掉约一半元素,n+n/2+n/4+…≈2n
最坏 O(n²)
每次划分都极不均衡,只减少一个元素,退化为逐个排除
坏运气来源
主元恰好总选到最小或最大,如已排序数组取固定位置主元
切西瓜对应 →quickSelect

每次对半切只需一刀;总切偏则逼近逐粒挑

T(n)=n+T(n/2)≈2nT(n)=n+T(n/2)\approx 2n
5第 5 页 · quickSelect 的性能

linearSelect 五步流程

linearSelect 五步走,用中位数的中位数保证每次划分都靠谱。

1
分组
每 5 个元素一组,最后一组不足 5 个也没关系
2
组内取中位数
每组 5 个,插入排序后取第 3 个,O(1) 完成
3
递归选中位数
对这些中位数再调 linearSelect,求出「中位数的中位数」
4
作主元划分
用它当 pivot 划分,保证主元不会太偏,最坏也均衡
5
向一侧递归
与 quickSelect 相同:只递归目标所在的一侧,规模至少砍三成
6第 6 页 · linearSelect 五步流程

中位数的中位数

先看分组结构,再看每层如何缩小范围:从中位数的中位数出发,理解为何能保证够好的划分。

图解渲染中…
a5:pivot ≈ 中位数,两侧至少各有约 3n/10 元素a6:保证最坏情况仍是 O(n)
7第 7 页 · 中位数的中位数

性能分析 A:递推式

上一页我们看到中位数的中位数能挑出好主元,但「好」到底好到什么程度?关键在于:主元把数组切开后,两边规模不能太大。这一页我们把它算成一笔账。

最坏切分
好主元保证一侧 ≤ 3n/4、另一侧 ≤ n/4,绝不出现极端偏斜
分组保障
每组 5 个取中位数,至少一半中位数 ≥ 主元,各压住约 3 个元素
递推式
消去一侧:只递归规模较大的一半,另一侧 discard,线性项来自划分
砍树选枝干对应 →消去一侧递归

只沿着粗的枝干继续追,细的那半直接扔掉不砍

T(n)≤T(3n/4)+T(n/5)+O(n),3n/4+n/5=19n/20<nT(n) \le T(3n/4) + T(n/5) + O(n),\qquad 3n/4 + n/5 = 19n/20 < n
8第 8 页 · 性能分析 A:递推式

性能分析 B:递推求解

用等比级数收紧递推式:两项系数之和小于 1,总和被封在 O(n)。

1
展开递推
T(n) ≤ T(n/5) + T(7n/10) + cn,取 c 覆盖划分开销
2
代入累加
逐层展开:每层剩 (1/5+7/10)=9/10 的规模,共 O(log n) 层
3
逐层求和
第 i 层开销 ≤ cn·(9/10)^i,构成公比 9/10 的等比级数
4
级数收敛
等比级数求和 ≤ cn·1/(1-9/10)=10cn,常数有界
5
得结论
T(n) = O(n),与公比 1/4+1/5<1 说法一致
9第 9 页 · 性能分析 B:递推求解

性能分析 C:常数与改进

前面我们证明了 linearSelect 严格 O(n),看起来完胜 quickSelect 的平均 O(n)。但大 O 只看增长趋势,把常数都抹掉了——而 linearSelect 恰恰藏着一个很大的常数。

隐藏的大常数
每层要做分组、排序、三趟划分,实际操作次数远超 quickSelect
quickSelect 为何快
随机划分代码极简,缓存友好,平均常数小,实践中常胜出
工程取舍
理论最坏保证与实际平均速度之间,需按场景选择
外卖 vs 自己做饭对应 →linearSelect vs quickSelect

外卖上不封顶但慢且贵,对应大 O 有保证但常数大

10第 10 页 · 性能分析 C:常数与改进

quickSelect vs linearSelect

两者都能期望线性选第 k 大,但一个靠概率、一个靠设计——混淆点在保证的强度不同。

quickSelect
  • 期望 O(n),随机主元实现简单
  • 最坏 O(n²),构造输入会退化
  • 常数小,缓存友好,实测更快
linearSelect
  • 严格 O(n),理论有硬保障
  • 最坏也是 O(n),无退化风险
  • 常数大,分组求中位数开销高
日常数据选 quickSelect;实时或对抗输入场景选 linearSelect。
11第 11 页 · quickSelect vs linearSelect

自测:主元质量

点击作答

linearSelect 选取的中位数的中位数作为主元,最关键的保证是什么?

12第 12 页 · 自测:主元质量

要点收束

  • ✓选取只需递归一侧,期望 O(n) 即可达成
  • ✓主元质量决定快选是否退化为 O(n²)
  • ✓中位数的中位数保证线性最坏情况
  • ✓确定性 O(n) 赢在最坏情况,常数却更大
  • ✓实际数据常让 quickSelect 更快
延伸主题:外存与流式场景下的选取多路划分与重复元素的处理选取在排序算法中的应用
13第 13 页 · 要点收束

课后思考

先自己想五分钟,再看参考答案——这些问题的价值在于想的过程。

1分组大小为什么恰好取 5?取 3 或 7 分别会怎样?

参考答案线索:推导分组大小 g 的递推式 T(n)≤T(n/g)+T(3n/4)+O(n),看 g=3 时 1/g 与 3/4 之和是否超过 1。5 恰好保证线性收敛,7 也行但每组找中位数更贵。

2linearSelect 常数很大,什么数据规模下它反而值得用?

参考答案从极端输入入手:如果主元选择反复被对抗性数据打偏,quickSelect 退化到 O(n²);对可预判恶意输入或需要严格最坏保证的系统,最坏线性时间本身就值这个常数。

3中位数的中位数这个技巧,还能迁移到哪些别的问题上?

参考答案它的本质是「确定性挑出质量有保证的分割点」。凡是依赖分割/剔除的问题都可借用,比如排序网络、平面最近点对的分界线选取、贪心删除类问题的批量筛除。

14第 14 页 · 课后思考