相关笔记

概览

本节介绍 SELECT 算法(也称 BFPRT 算法),它能在最坏情况下以 ==== 时间找到第 小元素。与 9.2 期望线性时间选择 的 RANDOMIZED-SELECT 不同,SELECT 通过确定性地选择一个”好”的枢轴(中位数的中位数)来保证每次划分都能有效缩小问题规模。该算法由 Blum、Floyd、Pratt、Rivest、Tarjan 五位计算机科学家于1973年提出。

要点列表:

  • SELECT 的最坏情况运行时间为 ====,是确定性的(非随机化)
  • 核心技巧:将数组分为 组,每组5个元素,取每组中位数,再取这些中位数的中位数作为枢轴
  • 关键保证:枢轴两侧各有至少 30% 的元素,确保递归子问题规模至多为
  • 递归关系式 ,解为
  • 实际中因常数因子大而较少直接使用,但理论价值极高

知识结构总览

flowchart TD
    A["9.3 最坏情况线性时间选择 (SELECT/BFPRT)"] --> B["算法流程"]
    A --> C["枢轴选择策略"]
    A --> D["最坏情况分析"]
    A --> E["算法特性"]

    B --> B1["预处理: 使元素数能被5整除"]
    B --> B2["分组排序: 每5个一组"]
    B --> B3["递归找中位数的中位数"]
    B --> B4["PARTITION-AROUND"]
    B --> B5["递归处理一侧"]

    C --> C1["5个一组排序"]
    C --> C2["取每组中位数"]
    C --> C3["递归取中位数的中位数"]

    D --> D1["引理: 至少30%元素在两侧"]
    D --> D2["递归关系式"]
    D --> D3["代入法求解 O(n)"]

    E --> E1["确定性: 无随机化"]
    E --> E2["最坏情况 Θ(n)"]
    E --> E3["常数因子大: 实际少用"]

核心思想

核心思路

RANDOMIZED-SELECT 的最坏情况 源于枢轴选择可能极差。SELECT 的解决思路是:用递归的方式找到一个确定性”好”的枢轴。具体策略是将数组分成每组5个元素,取每组中位数,再递归地取这些中位数的中位数。这个”中位数的中位数”保证了至少有30%的元素在枢轴的每一侧,从而确保最坏情况下递归子问题的规模至多为

直觉类比:假设你要在1000人中找到第500名(中位数)。你先让每5人一组排好队,取每组的第3名(中位数),得到200个”小组代表”。再从这200个代表中找到第100名(代表的中位数)。这个代表的中位数虽然不一定是1000人的真正中位数,但你能保证至少有300人比它小、至少有300人比它大——无论原始数据如何分布。

算法执行流程

  1. 预处理:通过 while 循环使元素数能被 5 整除(最多排除 4 个最小元素)
  2. 分组排序:将 n 个元素划分为 n/5 组,每组 5 个,对每组排序
  3. 取中位数:取出每组的中位数
  4. 递归选主元:递归调用 SELECT 找出中位数的中位数 x
  5. 划分:以 x 为主元调用 PARTITION-AROUND 划分数组
  6. 递归查找:根据 k 与主元位置的关系,在左半或右半部分递归查找
flowchart TD
    A["开始 SELECT"] --> B{"元素数能被5整除?"}
    B -- "否" --> C["找到最小元素并排除, p++, i--"]
    C --> B
    B -- "是" --> D["将元素分为 n/5 组, 每组排序"]
    D --> E["取出每组的中位数"]
    E --> F["递归 SELECT 找中位数的中位数 x"]
    F --> G["以 x 为主元划分数组"]
    G --> H{"i == k?"}
    H -- "是" --> I["返回 A[q]"]
    H -- "否" --> J{"i < k?"}
    J -- "是" --> K["递归: 左半部分"]
    J -- "否" --> L["递归: 右半部分, i 调整为 i-k"]

SELECT 完整伪代码

SELECT(A, p, r, i)
 1  while (r - p + 1) mod 5 ≠ 0
 2      for j = p + 1 to r              // 将最小元素放到 A[p]
 3          if A[p] > A[j]
 4              exchange A[p] with A[j]
 5      if i == 1                        // 如果要找最小值
 6          return A[p]
 7      p = p + 1                        // 排除最小值
 8      i = i - 1
 9  g = (r - p + 1) / 5                  // 5元素组的组数
10  for j = p to p + g - 1               // 对每组排序
11      sort ⟨A[j], A[j+g], A[j+2g], A[j+3g], A[j+4g]⟩ in place
12  // 所有组的中位数现在位于 A[p+2g : p+3g-1]
13  // 递归地找中位数的中位数作为枢轴
14  x = SELECT(A, p + 2g, p + 3g - 1, ⌈g/2⌉)
15  q = PARTITION-AROUND(A, p, r, x)     // 围绕枢轴划分
16  // 以下与 RANDOMIZED-SELECT 的第3-9行相同
17  k = q - p + 1
18  if i == k
19      return A[q]                      // 枢轴就是目标
20  elseif i < k
21      return SELECT(A, p, q - 1, i)    // 目标在左侧
22  else return SELECT(A, q + 1, r, i - k)  // 目标在右侧

SELECT

输入: 数组 个元素),整数 输出: 中第 小的元素

算法步骤:

  1. 预处理(第1-8行): 通过 while 循环使元素数能被5整除(最多执行4次),每次将最小元素放到 并排除
  2. 分组排序(第9-11行): 将元素分为 组,每组5个元素,用插入排序将每组排好序
  3. 选择枢轴(第14行): 递归调用 SELECT 找到 个组中位数的中位数
  4. 划分(第15行): 调用 PARTITION-AROUND 围绕 划分数组
  5. 递归搜索(第17-22行): 与 RANDOMIZED-SELECT 相同,判断枢轴是否为目标,否则递归处理一侧

逐行执行逻辑详解

第1-8行(预处理——使元素数能被5整除):

while 循环检查 ,即当前子数组的元素数不能被5整除。每次迭代:

  • 第2-4行:遍历 ,将最小元素交换到
  • 第5-6行:如果 (要找最小值),直接返回
  • 第7-8行:否则排除最小值, 加1, 减1

该循环最多执行4次(因为任何数模5的余数在0-4之间),因此是 次迭代。

第9-11行(分组排序):

  • 第9行:计算组数 (此时元素数已能被5整除)
  • 第10-11行:对每组排序。第 组的5个元素为
  • 排序后每组满足:
  • 每组用插入排序,耗时 (因为只有5个元素),共 组,总时间

第14行(递归选择枢轴):

  • 每组的中位数是 (5个元素中的第3个)
  • 所有组的中位数位于
  • 递归调用 SELECT(A, p + 2g, p + 3g - 1, ⌈g/2⌉) 找到这些中位数的中位数
  • 这就是著名的**“中位数的中位数”**(median of medians)

第15行(围绕枢轴划分):

  • PARTITION-AROUND(A, p, r, x) 类似 7.1 快速排序的描述 的 PARTITION,但以指定元素 为枢轴
  • 返回枢轴的最终位置 ,使得

第17-22行(递归搜索):

9.2 期望线性时间选择 的 RANDOMIZED-SELECT 完全相同:

  • 计算 (枢轴排名)
  • :返回枢轴
  • :递归左侧
  • :递归右侧(注意 变为

PARTITION-AROUND 说明

PARTITION-AROUND

PARTITION-AROUND(A, p, r, x) 是 PARTITION 的变体,以指定元素 为枢轴进行划分(而非默认使用 )。

实现方式: 先找到 中的位置,将其与 交换,然后执行标准的 PARTITION 过程。

返回值: 枢轴 的最终位置 ,使得

时间复杂度: ,与 PARTITION 相同。


最坏情况运行时间分析

枢轴质量保证——至少30%元素在两侧

引理(枢轴两侧的元素数量)

设 SELECT 选择枢轴 为中位数的中位数, 为5元素组的组数。则:

  • 至少有 个元素 (黄色区域)
  • 至少有 个元素 (蓝色区域)

因此,划分后每一侧的子数组至多包含 个元素。

证明:

【分组计数( 的元素)】 至少 组的中位数 ,每组贡献 个元素

考虑图9.3所示的元素关系。有 组,每组5个元素已排好序。

黄色区域( 的元素):

  • 个组中位数的中位数,因此至少有 个组的中位数
  • 对于每个中位数 的组,该组中至少有3个元素 其中位数(中位数本身及其上方两个元素)
  • 因此至少有 个元素

【分组计数( 的元素)】 对称地,至少 个元素

蓝色区域( 的元素):

  • 至少有 个组的中位数
  • 对于每个中位数 的组,该组中至少有3个元素 其中位数(中位数本身及其下方两个元素)
  • 因此至少有 个元素

【两侧子问题规模上界()】 排除保证元素后,每侧至多 个元素

两侧子问题规模上界:

  • 总元素数为
  • 低侧()排除黄色区域的元素,至多有 个元素
  • 高侧()排除蓝色区域的元素,同样至多有 个元素

递归关系式的建立

递归关系式

定义 为 SELECT 在至多 个元素的输入上的最坏情况运行时间。 单调不减。

非递归部分:

  • while 循环(第1-8行): 次迭代,每次 ,共
  • 分组排序(第10-11行): 组,每组 ,共
  • PARTITION-AROUND(第15行):
  • 其他簿记操作:
  • 合计:

递归部分:

  • 找枢轴(第14行):(因为 单调不减)
  • 递归搜索(第21或22行):至多 (由上面的引理)

完整递归关系式:

代入法求解

定理 9.3(Theorem 9.3)

SELECT 在 个元素输入上的运行时间为

证明:

【代入法(猜测 )】 假设线性上界,代入递归关系式验证

代入法(substitution method)证明 ,其中 是足够大的正常数。

归纳假设: 对所有 ,有

【归纳步骤(代入递归式)】 代入

归纳步骤: 假设 ,将归纳假设代入递归关系式:

【选取常数 保证收敛)】 的”余量”吸收低阶项

要使 ,需要:

足够大,使得 主导 中的常数时,该不等式成立。

基础情况: 选择 足够大,使得对所有 (即 ,SELECT 内部递归的基础情况), 成立。

下界: 第11行的分组排序本身就需要 时间,因此

结论:

为什么选择5作为组的大小?

组大小选择的分析

选择组大小为5是算法设计中的一个精妙平衡:

组大小 排序每组耗时递归找中位数耗时每侧保证的元素比例递归关系
3
5
7

关键观察: 组大小为3时,,不满足主定理情况3的条件(需要严格小于1),因此无法保证线性时间。组大小为5时,,可以保证线性时间。

组大小为5是满足线性时间保证的最小奇数,这使得排序每组的开销最小化(偶数组的中位数定义不够直接)。


补充理解与拓展

BFPRT 算法的历史与五位图灵奖级别的作者

SELECT 算法由五位计算机科学家于1973年在论文 “Time Bounds for Selection”(Journal of Computer and System Sciences, Vol. 7, pp. 448-461)中提出,因此得名 BFPRT(取五位作者姓氏首字母):

作者主要贡献后续荣誉
Manuel Blum计算复杂性理论先驱图灵奖(1995年)
Robert FloydFloyd-Warshall 算法、堆排序分析图灵奖(1978年)
Vaughan PrattPratt 素性测试、Knuth-Morris-Pratt 算法
Ronald RivestRSA 加密算法、CLRS 合著者图灵奖(2002年)
Robert TarjanTarjan SCC 算法、LCA、Splay Tree图灵奖(1986年)

一篇论文中有三位图灵奖得主,这在计算机科学史上极为罕见。BFPRT 的理论意义在于:它证明了选择问题可以在最坏情况下线性时间内解决,这一结果与排序问题的 下界形成鲜明对比,说明选择问题本质上比排序问题”更容易”。

来源:Blum, Floyd, Pratt, Rivest, Tarjan, “Time Bounds for Selection”, JCSS, 1973

Introselect——工程实践中的最优策略

虽然 BFPRT 保证了最坏情况 ,但其常数因子约为 Quickselect 的 5-10倍,在实际工程中很少直接使用。Introselect(Musser, 1997)巧妙地结合了两者的优势:

Introselect 策略:

  1. 先使用 Quickselect(9.2 期望线性时间选择),期望运行快
  2. 监控递归深度;如果深度超过阈值(如 ),说明可能遇到最坏情况
  3. 切换到 BFPRT 的枢轴选择策略,保证最坏情况线性时间

实际应用:

  • C++ STL 的 std::nth_element 通常使用 Introselect 而非纯 Quickselect
  • 许多标准库实现中,Introselect 的性能接近 Quickselect,同时具有 BFPRT 的最坏情况保证
  • 这种”先用快速方法,遇到退化时切换到安全方法”的思想也体现在 Introsort(快速排序 + 堆排序的混合)中

性能对比(典型场景):

算法期望时间最坏时间常数因子
Quickselect小(约1x)
BFPRT大(约5-10x)
Introselect小(约1-2x)

来源:Musser, D.R., “Introspective Sorting and Selection Algorithms”, Software: Practice and Experience, Vol. 27(8), 1997


易混淆点与辨析

误区:SELECT 比 RANDOMIZED-SELECT 总是更好

错误理解: “SELECT 的最坏情况是 ,RANDOMIZED-SELECT 的最坏情况是 ,所以 SELECT 总是更好的选择”

正确理解: SELECT 的最坏情况确实优于 RANDOMIZED-SELECT,但 SELECT 的常数因子远大于 RANDOMIZED-SELECT(约5-10倍)。在绝大多数实际场景中,RANDOMIZED-SELECT(或 Introselect)的期望性能远优于 SELECT。

选择建议:

  • 一般情况: 使用 RANDOMIZED-SELECT 或 Introselect(如 std::nth_element
  • 需要确定性保证: 使用 SELECT(如实时系统中有严格延迟上界要求)
  • 理论分析: SELECT 的价值在于证明选择问题可以在最坏情况线性时间内解决

这类似于快速排序 vs 堆排序的关系:快速排序期望更快,但堆排序有更好的最坏情况保证。

误区:中位数的中位数就是全局中位数

错误理解: “中位数的中位数(median of medians)就是数组的中位数,所以 SELECT 总是能一步到位”

正确理解: 中位数的中位数不一定是全局中位数。它只是一个保证质量的枢轴——我们只能保证至少有30%的元素在它每一侧,而非精确的50%。

具体例子: 考虑数组 (已排序)。

  • 分为3组:
  • 组中位数:
  • 中位数的中位数:
  • 全局中位数也是 (巧合)

但对于数组

  • 分为3组:
  • 组中位数:
  • 中位数的中位数:
  • 全局中位数也是 (仍然是巧合,但枢轴质量差——几乎所有元素都 枢轴)

关键在于:虽然中位数的中位数不精确,但30%的保证足以推导出线性时间


习题精选

题号题目描述难度
9.3-1证明 SELECT 在组大小为7时仍为线性时间⭐⭐
9.3-2用基础情况替代预处理,分析修改后的递归关系式⭐⭐
9.3-3使用 SELECT 作为子程序使快速排序最坏情况 ⭐⭐⭐
9.3-5证明5元素集合的中位数可用6次比较找到⭐⭐
9.3-7油管道问题:线性时间确定最优管道位置⭐⭐⭐

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 6Order Statistics, Medianhttps://www.youtube.com/watch?v=8BiVbP15G1oMIT 公开课,讲解中位数的中位数思想
Abdul BariSelection Sort, Quickselecthttps://www.youtube.com/watch?v=FN4wGLBiAOQ含 BFPRT 算法的基本思路
HackerRankMedian of Medianshttps://www.youtube.com/watch?v=YU1HfMTEtGA逐步演示中位数的中位数选择过程
WilliamFisetMedian of Medianshttps://www.youtube.com/watch?v=YU1HfMTEtGA算法系列教程,含复杂度分析
Tim RoughgardenAlgorithms Illuminatedhttps://www.youtube.com/watch?v=0vqGm_yMpdAStanford 教授,深入浅出讲解选择问题

教材原文

CLRS 第4版 9.3节原文

We’ll now examine a remarkable and theoretically interesting selection algorithm whose running time is in the worst case. Although the RANDOMIZED-SELECT algorithm from Section 9.2 achieves linear expected time, we saw that its running time in the worst case was quadratic. The selection algorithm presented in this section achieves linear time in the worst case, but it is not nearly as practical as RANDOMIZED-SELECT. It is mostly of theoretical interest.

Like the expected linear-time RANDOMIZED-SELECT, the worst-case linear-time algorithm SELECT finds the desired element by recursively partitioning the input array. Unlike RANDOMIZED-SELECT, however, SELECT guarantees a good split by choosing a provably good pivot when partitioning the array. The cleverness in the algorithm is that it finds the pivot recursively. Thus, there are two invocations of SELECT: one to find a good pivot, and a second to recursively find the desired order statistic.


参见Wiki

第09章-中位数与序统计 最坏情况线性时间选择