排序算法之:Median of Medians(BFPRT)算法——线性时间选择与分组优化
字数 3047 2025-12-15 02:10:44

排序算法之: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 的核心思想

  • 目标:选择一个主元,使得在分区后,递归调用的子问题规模至少缩减一定比例。
  • 步骤概览
    1. 分组:将数组分成每组5个元素的小组(最后一组可能不足5个)。
    2. 求各组中位数:对每个小组内部排序,取出其中位数。
    3. 递归求中位数的中位数:对这些中位数组成的数组,递归调用本算法,求出其中位数(即“中位数的中位数”,简称MoM)。
    4. 以此MoM作为主元,进行分区(类似快速排序的分区操作)。
    5. 判断递归方向:根据分区后主元的位置与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 的元素个数。
  • 比较 kLL + (等于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
    • pivotmedians 的中位数,故至少有一半的组(即 ⌈⌈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) 为算法在最坏情况下的运行时间。
  • 递归分为两部分:
    1. 求 MoM:T(⌈n/5⌉)
    2. 分区后递归:T(7n/10 + 6)
  • 加上线性操作:分组、求各组中位数(每组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) 的时间复杂度。它虽然常数因子较大(实际中可能比快速选择慢),但在对最坏性能有严格要求的场景中不可或缺。掌握其分组、递归及复杂度分析,是深入理解线性时间选择算法的关键。

排序算法之: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) 。 加上线性操作:分组、求各组中位数(每组5个排序为常数时间)、分区,总线性开销为 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) 的时间复杂度。它虽然常数因子较大(实际中可能比快速选择慢),但在对最坏性能有严格要求的场景中不可或缺。掌握其分组、递归及复杂度分析,是深入理解线性时间选择算法的关键。