排序算法之:Median of Medians(BFPRT)算法——线性时间选择与分组优化
题目描述
Median of Medians(又称BFPRT算法)是一种用于在未排序数组中找到第k小(或第k大)元素的确定性算法,且最坏情况时间复杂度为 O(n)。它不仅是快速选择(Quickselect)算法的优化版本,还广泛应用于需要保证最坏性能的场景(如系统调度、数据库查询优化)。本题目要求深入理解其分组策略、递归调用及复杂度证明。
解题步骤详解
1. 问题定义与直观思路
- 输入:一个包含n个元素的数组
A,和一个整数k(1 ≤ k ≤ n)。 - 输出:数组中第k小的元素。
- 简单方法的问题:
- 排序后取第k个:O(n log n),不够高效。
- 快速选择(Quickselect):平均O(n),但若主元(pivot)选择不当(如总是最小/最大元素),最坏会退化到O(n²)。
核心挑战:如何确定性地选择一个“足够好”的主元,避免最坏情况?
2. Median of Medians 的核心思想
- 目标:选择一个主元,使得在分区后,递归调用的子问题规模至少缩减一定比例。
- 步骤概览:
- 分组:将数组分成每组5个元素的小组(最后一组可能不足5个)。
- 求各组中位数:对每个小组内部排序,取出其中位数。
- 递归求中位数的中位数:对这些中位数组成的数组,递归调用本算法,求出其中位数(即“中位数的中位数”,简称MoM)。
- 以此MoM作为主元,进行分区(类似快速排序的分区操作)。
- 判断递归方向:根据分区后主元的位置与k的关系,递归地在左子数组或右子数组中继续查找。
3. 算法步骤的细节展开
步骤1:分组
- 将数组
A[0...n-1]分成 ⌈n/5⌉ 组,每组最多5个元素(最后一组可能少于5个)。 - 例如:n=23,则分成5组:前4组各5元素,最后一组3元素。
为什么是5?
分组大小必须为奇数以保证中位数定义明确,且需在递归复杂度与分组开销间平衡。5是最小的奇数值,能保证线性复杂度。
步骤2:求各组中位数
- 对每组内部进行插入排序(因为规模仅5,插入排序效率高且稳定),然后取出每组的中位数(即第3个元素)。
- 设中位数数组为
medians[],长度为m = ⌈n/5⌉。
步骤3:递归求中位数的中位数
- 在
medians[]数组中,递归调用select(medians, 0, m-1, m/2)找到其中位数(即第⌊m/2⌋小的元素)。这个值就是 MoM,记为pivot。
注意:这是算法中的递归调用,但问题规模从 n 缩小到了 ⌈n/5⌉。
步骤4:分区
- 使用
pivot作为主元,将原数组A进行分区(类似于快速排序的Lomuto或Hoare分区):- 将数组划分为三部分:
< pivot、= pivot、> pivot。 - 返回主元在分区后的位置索引
pos,以及等于主元的区间[left_eq, right_eq](若存在重复值)。
- 将数组划分为三部分:
步骤5:递归选择
- 设
L为< pivot的元素个数。 - 比较
k与L及L + (等于pivot的元素个数):- 若
k ≤ L:递归在< pivot的部分中找第k小元素。 - 若
L < k ≤ L + (等于pivot的个数):则pivot就是答案。 - 否则:递归在
> pivot的部分中找第k - L - (等于pivot的个数)小的元素。
- 若
4. 关键证明:为什么时间复杂度是 O(n)?
分组规模分析
- 每组5个元素,排序后取中位数。关键性质:在
medians[]中选出的pivot(即MoM)至少大于等于30%的元素,也至少小于等于30%的元素。 - 推导:
- 有
⌈n/5⌉个组,每组中位数组成数组medians。 pivot是medians的中位数,故至少有一半的组(即⌈⌈n/5⌉/2⌉组)的中位数 ≤pivot。- 在每个这样的组中,至少3个元素(包括中位数本身及比它小的两个)≤
pivot。 - 因此,至少有
3 * ⌈⌈n/5⌉/2⌉ ≥ 3⌈n/10⌉个元素 ≤pivot。 - 同理,至少有
3⌈n/10⌉个元素 ≥pivot。 - 故在分区后,递归调用的子问题规模最多为
n - 3⌈n/10⌉ ≤ 7n/10 + 6。
- 有
递归复杂度推导
- 设 T(n) 为算法在最坏情况下的运行时间。
- 递归分为两部分:
- 求 MoM:
T(⌈n/5⌉)。 - 分区后递归:
T(7n/10 + 6)。
- 求 MoM:
- 加上线性操作:分组、求各组中位数(每组5个排序为常数时间)、分区,总线性开销为
c·n。 - 递归方程:
T(n) ≤ T(⌈n/5⌉) + T(7n/10 + 6) + c·n - 用代入法或主定理可证明 T(n) = O(n)。
直观理解:每次递归,问题规模至少缩减30%(至多原来的7/10),且每次递归调用的开销(求MoM)规模缩减为原来的1/5,总开销线性。
5. 示例演示(查找第k小的元素)
假设数组 A = [12, 3, 5, 7, 4, 19, 10, 2, 8, 6],n=10,k=4(找第4小的数)。
步骤1:分组(每组5元素)
组1: [12, 3, 5, 7, 4]
组2: [19, 10, 2, 8, 6]
步骤2:求各组中位数
- 组1排序: [3,4,5,7,12] → 中位数=5
- 组2排序: [2,6,8,10,19] → 中位数=8
medians = [5, 8]
步骤3:求MoM
对 medians 递归调用 select(medians, 0, 1, 1)(找第1小的数),返回5。
pivot = 5
步骤4:分区
以5为主元分区:[3,4,2,5,12,19,10,7,8,6]
<5的部分: [3,4,2](3个元素)=5的部分: [5](1个元素)>5的部分: [12,19,10,7,8,6](6个元素)
主元位置索引pos = 3(从0开始)。
步骤5:递归选择
k=4,而 L=3(小于5的元素个数),且等于5的元素个数=1。
因为 L < k ≤ L+1(即3 < 4 ≤ 4),所以主元5就是答案。
结果:第4小的元素是5。
6. 算法实现注意事项
- 尾递归优化:递归调用仅有一次(分区后选择一边),可转换为循环减少栈深度。
- 小规模情况处理:当 n ≤ 5(或较小阈值)时,直接插入排序返回第k个元素,作为递归基。
- 重复元素处理:分区时需统计等于主元的元素个数,避免重复值导致递归方向错误。
- 空间复杂度:除了递归栈,只需 O(1) 额外空间(原地分区)。
7. 应用场景
- 系统调度中的优先级选择。
- 数据库查询优化中快速取中位数或百分位数。
- 统计学中的稳健估计(如中位数绝对偏差)。
总结
Median of Medians 算法通过巧妙的五元素分组策略和递归求中位数的中位数,保证了主元选择的品质,从而在最坏情况下依然实现 O(n) 的时间复杂度。它虽然常数因子较大(实际中可能比快速选择慢),但在对最坏性能有严格要求的场景中不可或缺。掌握其分组、递归及复杂度分析,是深入理解线性时间选择算法的关键。