相关笔记
- 前置笔记:7.1 快速排序的描述(PARTITION)、5.2 指示器随机变量、随机化算法
- 关联概念:分治法、递归关系式
- 后续笔记:9.3 最坏情况线性时间选择
- 章节汇总:第09章_中位数与序统计-章节汇总
概览
本节介绍 RANDOMIZED-SELECT 算法(也称 Quickselect),它是基于快速排序思想的选择算法。与快速排序递归处理分区两侧不同,RANDOMIZED-SELECT 每次只递归处理一侧,因此期望运行时间为 ====,而非快速排序的 。该算法由 Hoare 于1961年与快速排序同时发明。
要点列表:
- RANDOMIZED-SELECT 的期望运行时间为 ====,最坏情况为
- 算法核心:利用 7.1 快速排序的描述 的 RANDOMIZED-PARTITION 划分数组,然后只递归处理包含目标元素的一侧
- 期望线性时间的证明依赖于指示器随机变量和”有益划分”(helpful partitioning)的概念
- 与排序后取第 小元素相比(),Quickselect 在只需要单个序统计量时更高效
知识结构总览
flowchart TD A["9.2 期望线性时间选择 (RANDOMIZED-SELECT)"] --> B["算法流程"] A --> C["与快速排序的对比"] A --> D["期望运行时间分析"] A --> E["算法特性"] B --> B1["基线条件: p == r"] B --> B2["RANDOMIZED-PARTITION"] B --> B3["判断 pivot 是否为目标"] B --> B4["递归处理一侧"] C --> C1["两侧递归 vs 单侧递归"] C --> C2["Θ(n lg n) vs Θ(n)"] D --> D1["引理9.1: 有益划分概率 ≥ 1/2"] D --> D2["定理9.2: 期望 Θ(n)"] D --> D3["代数法证明"] E --> E1["最坏情况 Θ(n²)"] E --> E2["随机化: 无特定输入触发最坏情况"] E --> E3["原地操作"]
核心思想
核心思路
RANDOMIZED-SELECT 的关键洞察在于:选择问题只需要找到一个元素,不需要对两侧都排序。快速排序每次划分后递归处理两侧,产生 的期望时间;而选择算法每次只需递归处理一侧,期望递归深度为常数级,因此总期望时间为 。
直觉类比:假设你要在一本1000页的字典中找第500个词。快速排序相当于把字典完全整理好(两侧都排),而选择算法相当于每次翻到随机一页,判断目标在左半部分还是右半部分,然后只翻那一半——平均只需翻几次就能找到。
算法执行流程
- 基线检查:若 p == r,子数组只有一个元素,直接返回 A[p]
- 随机划分:调用 RANDOMIZED-PARTITION,随机选择主元 q 并划分数组
- 计算排名:k = q - p + 1,确定主元在当前子数组中的排名
- 判断目标位置:若 i == k,主元恰好是目标,返回 A[q]
- 递归查找:若 i < k 则在左半部分递归查找;若 i > k 则在右半部分递归查找(i 调整为 i - k)
flowchart TD A["开始 RANDOMIZED-SELECT"] --> B{"p == r?"} B -- "是" --> C["返回 A[p]"] B -- "否" --> D["q = RANDOMIZED-PARTITION"] D --> E["k = q - p + 1"] E --> F{"i == k?"} F -- "是" --> G["返回 A[q]"] F -- "否" --> H{"i < k?"} H -- "是" --> I["递归: 左半部分 A[p..q-1], i 不变"] H -- "否" --> J["递归: 右半部分 A[q+1..r], i 调整为 i-k"]
RANDOMIZED-SELECT 伪代码
RANDOMIZED-SELECT(A, p, r, i)
1 if p == r
2 return A[p] // 基线条件:只有一个元素
3 q = RANDOMIZED-PARTITION(A, p, r) // 随机选择枢轴并划分
4 k = q - p + 1 // 枢轴是第 k 小元素
5 if i == k
6 return A[q] // 枢轴恰好是目标
7 elseif i < k
8 return RANDOMIZED-SELECT(A, p, q - 1, i) // 目标在左侧
9 else return RANDOMIZED-SELECT(A, q + 1, r, i - k) // 目标在右侧
RANDOMIZED-SELECT
输入: 数组 ( 个元素),整数 () 输出: 中第 小的元素
算法步骤:
- 基线条件: 若 ,子数组只有一个元素,直接返回
- 划分: 调用
RANDOMIZED-PARTITION(A, p, r),随机选择枢轴并将数组划分为 和 ,使得- 判断: 计算 (枢轴在当前子数组中的排名)
- 若 :枢轴就是第 小元素,返回
- 若 :目标在左侧子数组,递归搜索 中的第 小
- 若 :目标在右侧子数组,递归搜索 中的第 小
逐行执行逻辑详解
第1-2行(基线条件):
- 当 时,子数组 只包含一个元素
- 此时 ,所以 必须等于
- 直接返回 即可
第3行(随机划分):
- 调用
RANDOMIZED-PARTITION(见 7.1 快速排序的描述 第7.3节) - 该过程随机选择一个元素作为枢轴,将数组划分为两部分
- 返回值 是枢轴的最终位置
- 划分后:
第4行(计算排名):
- 表示枢轴 在子数组 中是第 小的元素
- 因为 共有 个元素,且都
第5-6行(命中检查):
- 若 ,说明我们要找的第 小元素恰好就是枢轴
- 直接返回
第7-8行(左侧递归):
- 若 ,第 小元素在 中
- 递归调用
RANDOMIZED-SELECT(A, p, q-1, i) - 注意: 不变,因为我们在更小的子数组中仍然找第 小
第9行(右侧递归):
- 若 ,第 小元素在 中
- 递归调用 `RANDOMIZED-SELECT(A, q+1, r, i-k)$
- 关键: 变为 ,因为左侧有 个比目标更小的元素已被”跳过”
与快速排序的对比
RANDOMIZED-SELECT vs RANDOMIZED-QUICKSORT
比较维度 RANDOMIZED-SELECT RANDOMIZED-QUICKSORT 递归策略 只递归处理一侧 递归处理两侧 期望时间 最坏时间 输出 第 小元素 完整排序数组 划分过程 相同(RANDOMIZED-PARTITION) 相同(RANDOMIZED-PARTITION) 为什么只递归一侧就能从 降到 ?
快速排序的递归树深度为 ,每层总工作量为 ,因此总时间为 。而 RANDOMIZED-SELECT 的递归树是一条单链(每次只走一侧),虽然链的期望长度不是 ,但由于每次划分后子问题期望缩小一个常数比例,总工作量形成一个几何递减序列,其和为 。
期望运行时间分析
直觉理解
直觉
假设每次随机选择的枢轴都落在排序后数组的”中间一半”(第二和第三四分位数之间)。那么每次划分后,至少有 的元素被排除在后续递归之外,最多只有 的元素仍在考虑中。此时递归关系为 ,由主定理情况3可得 。
实际上,枢轴不一定每次都落在中间一半。但由于枢轴是随机选择的,落在中间一半的概率约为 。因此期望约每两次划分就能获得一次”有益划分”,期望运行时间仍为 。
有益划分(Helpful Partitioning)
有益划分
定义第 次划分后仍在考虑中的元素集合为 ,其中 为全部元素。若一次划分满足
则称该划分为有益划分(helpful partitioning)。
引理 9.1:有益划分的概率
引理 9.1(Lemma 9.1)
一次划分是有益划分的概率至少为 。
证明:
【充分性(中间一半 有益划分)】 枢轴落入中间一半时,至少 个元素被排除
定义 元素子数组的”中间一半”(middle half)为:去掉最小的 个元素和最大的 个元素后剩余的元素集合。
第一步:证明枢轴落入中间一半 ⇒ 有益划分。
无论枢轴落在哪个位置,划分后要么所有大于枢轴的元素、要么所有小于枢轴的元素(连同枢轴本身)将不再被考虑。若枢轴落入中间一半,则至少有 个小于枢轴的元素或 个大于枢轴的元素,加上枢轴本身,至少有 个元素不再被考虑。
因此剩余元素至多为:
所以划分是有益的。
【必要性(概率下界 )】 计算枢轴不在中间一半的概率上界
第二步:证明枢轴落入中间一半的概率至少为 。
枢轴不落入中间一半的概率为:
因此:
定理 9.2:期望运行时间
定理 9.2(Theorem 9.2)
RANDOMIZED-SELECT 在 个不同元素的输入数组上的期望运行时间为 。
证明:
【代分组(按有益划分分代)】 将划分序列按有益划分分组,每代集合大小至多乘以
将所有划分按”有益划分”分组为代(generation)。设 为有益划分的索引序列,其中 (将初始状态视为一次”虚拟”有益划分)。
定义第 代的元素集合大小为 ,其中 为原始问题规模。由于每次有益划分后集合大小至多为原来的 :
经过 次有益划分后,只剩下一个元素。
【几何分布(期望代内划分次数 )】 有益划分概率 ,期望等待次数
定义随机变量 ,即第 代中的划分次数。由引理 9.1,每次划分是有益划分的概率至少为 ,因此 服从几何分布(或被其控制),期望值:
比较次数上界: 第 次划分将枢轴与 个其他元素比较,因此比较次数少于 。第 代中的集合大小至多为 ,该代有 次划分,因此第 代的总比较次数少于 。
【期望线性性质 + 无穷等比级数】 利用 求和
总比较次数期望值:
因此期望比较次数为 。又因为第一次 RANDOMIZED-PARTITION 就需要检查所有 个元素,下界为 。
结论: RANDOMIZED-SELECT 的期望运行时间为 。
最坏情况分析
最坏情况
RANDOMIZED-SELECT 的最坏情况发生在每次划分都极不走运——枢轴总是当前子数组的最大或最小元素。此时每次递归只排除一个元素(枢轴本身),递归关系为:
解为 。
但注意: 由于算法是随机化的,没有特定输入会始终触发最坏情况行为。对任何固定输入,期望运行时间都是 。
补充理解与拓展
Quickselect 的工程应用与历史
Quickselect(RANDOMIZED-SELECT)由 Tony Hoare 于1961年在其经典论文 “Quicksort”(Computer Journal, Vol. 5, pp. 10-16)中与快速排序同时提出,也称为 “Hoare’s selection algorithm”。该算法是选择问题在实际工程中最广泛使用的解决方案。
主要工程应用:
应用场景 实现方式 说明 C++ STL std::nth_element标准库直接提供,通常基于 Introselect(Quickselect + BFPRT 的混合策略) Python statistics.median对小数组使用排序,对大数组可能使用 Quickselect 变体 数据库引擎 MEDIAN 聚合函数 PostgreSQL、MySQL 等使用 Quickselect 或其变体计算中位数 流处理 近似中位数算法 Greenwald-Khanna (2001) 提出的单遍近似中位数算法,适用于无法存储全部数据的数据流场景 Quickselect vs 排序后取第 个:
- 排序需要 ,Quickselect 期望
- 当只需要一个序统计量时,Quickselect 更高效
- 当需要多个序统计量或完整排序时,直接排序更合适
来源:Hoare, C.A.R., “Quicksort”, Computer Journal, 1962; Musser, D.R., “Introspective Sorting and Selection Algorithms”, Software: Practice and Experience, 1997; Greenwald & Khanna, “Space-efficient online computation of quantile summaries”, SIGMOD 2001
指示器随机变量技术——期望分析的核心工具
定理 9.2 的证明使用了与 5.2 指示器随机变量 和 7.1 快速排序的描述 第7.4节相同的分析技术——指示器随机变量(indicator random variable)。该技术是分析随机化算法期望运行时间的核心工具。
技术要点:
- 将随机事件映射为指示器随机变量
- 利用期望的线性性质:
- 每个指示器的期望等于对应事件发生的概率:
在 RANDOMIZED-SELECT 分析中的具体应用:
- 定义 为第 代中的划分次数(几何分布随机变量)
- 利用 (因为有益划分概率 )
- 将总比较次数表示为 ,利用期望线性性求和
这一技术也广泛应用于:
- 快速排序期望比较次数分析(第7.4节)
- 散列表期望查找时间分析(第11章)
- 跳表期望搜索时间分析
来源:Cormen et al., Introduction to Algorithms, 4th ed., Section 5.2; Motwani & Raghavan, Randomized Algorithms, Cambridge University Press, 1995
易混淆点与辨析
误区:RANDOMIZED-SELECT 的最坏情况 意味着它不可靠
❌ 错误理解: “RANDOMIZED-SELECT 最坏情况是 ,和直接排序差不多,还不如排序后取第 个”
✅ 正确理解: RANDOMIZED-SELECT 是随机化算法,最坏情况 只在极端不走运时发生,概率极低。对任何固定输入,期望运行时间都是 。不存在特定输入能始终触发最坏情况。
对比: 非随机化的选择算法(如每次选第一个元素作枢轴)确实存在特定输入(如已排序数组)会始终触发最坏情况。随机化的优势在于将最坏情况从”输入依赖”变为”概率事件”。
如果需要确定性的最坏情况 保证,应使用 9.3 最坏情况线性时间选择 中的 SELECT 算法。
误区:RANDOMIZED-SELECT 递归调用可能传入空数组
❌ 错误理解: “当 时递归调用
RANDOMIZED-SELECT(A, p, q-1, i),如果 则传入空数组,会出错”✅ 正确理解: RANDOMIZED-SELECT 永远不会对空数组进行递归调用。
证明: 若 ,则 。此时第5行检查 即 ,直接返回 ,不会进入第7-9行的递归分支。类似地,若 ,则 ,此时 ,不会进入 的分支。因此递归调用的子数组始终至少包含一个元素。
习题精选
| 题号 | 题目描述 | 难度 |
|---|---|---|
| 9.2-1 | 证明 RANDOMIZED-SELECT 永远不会对空数组进行递归调用 | ⭐ |
| 9.2-2 | 写出 RANDOMIZED-SELECT 的迭代版本 | ⭐⭐ |
| 9.2-3 | 描述一个使 RANDOMIZED-SELECT 在数组 上产生最坏情况性能的划分序列 | ⭐⭐ |
| 9.2-4 | 论证 RANDOMIZED-SELECT 的期望运行时间不依赖于输入数组中元素的排列顺序 | ⭐⭐⭐ |
9.2-1 解答
目标: 证明 RANDOMIZED-SELECT 永远不会对空数组进行递归调用。
证明:
【情况分析(两个递归调用点)】 分别验证第8行和第9行递归调用不会传入空数组
RANDOMIZED-SELECT 有两个递归调用点:
- 第8行:
RANDOMIZED-SELECT(A, p, q-1, i)(当 时)- 第9行:
RANDOMIZED-SELECT(A, q+1, r, i-k)(当 时)情况一:第8行的递归调用。 此时 ,即 。由于 ,有 ,即 。因此子数组 至少包含 个元素,不会为空。
情况二:第9行的递归调用。 此时 ,即 。由于 ,有 ,即 。因此子数组 至少包含 个元素,不会为空。
结论: RANDOMIZED-SELECT 永远不会对空数组进行递归调用。
9.2-3 解答
目标: 描述使 RANDOMIZED-SELECT 产生最坏情况 的划分序列。
分析: 最坏情况发生在每次划分都只排除一个元素(枢轴本身)。假设我们要找最小元素()。
对数组 ,最坏情况划分序列为:
- 第一次划分: 枢轴选为 (最大元素),划分后 ,。,递归左侧。
- 第二次划分: 枢轴选为 (左侧子数组最大元素),划分后排除 ,递归剩余 8 个元素。
- 第三次划分: 枢轴选为 ,递归剩余 7 个元素。
- 第四次划分: 枢轴选为 ,递归剩余 6 个元素。
- 第五次划分: 枢轴选为 ,递归剩余 5 个元素。
- 第六次划分: 枢轴选为 ,递归剩余 4 个元素。
- 第七次划分: 枢轴选为 ,递归剩余 3 个元素。
- 第八次划分: 枢轴选为 ,递归剩余 2 个元素。
- 第九次划分: 枢轴选为 ,递归剩余 1 个元素。
- 返回 (最小元素)。
每次只排除一个元素,总比较次数为 。
注意: 由于 RANDOMIZED-PARTITION 是随机选择枢轴的,这种最坏情况序列发生的概率极低(),实际中几乎不会遇到。
9.2-4 解答
目标: 证明 RANDOMIZED-SELECT 的期望运行时间不依赖于输入数组中元素的排列顺序。
证明(对输入长度 进行数学归纳):
【归纳法(基础步 + 归纳步)】 对输入长度 归纳,证明期望时间与排列无关
基础情况(): 当 时,算法直接返回 ,运行时间为 ,与元素值无关。
归纳假设: 对所有长度小于 的子数组,RANDOMIZED-SELECT 的期望运行时间相同,不依赖于元素的排列顺序。
【均匀随机选择(排列无关性)】 枢轴均匀随机选取,期望时间对所有排列相同
归纳步骤: 考虑长度为 的子数组 。
RANDOMIZED-PARTITION 首先从 中均匀随机选择一个元素作为枢轴。无论输入数组如何排列,每个位置上的元素被选为枢轴的概率都是 。
设 为输入排列为 时的期望运行时间。则:
其中 表示枢轴为第 小元素时的排列, 和 分别是左右子问题的运行时间。
由归纳假设, 和 不依赖于具体排列。因此 对所有排列 都相同。
视频学习指南
| 资源 | 主题 | 链接 | 说明 |
|---|---|---|---|
| MIT 6.006 Lecture 6 | Selection Sort, Quickselect | https://www.youtube.com/watch?v=8BiVbP15G1o | MIT 公开课,讲解 Quickselect 的核心思想与期望分析 |
| Abdul Bari | Quick Select Algorithm | https://www.youtube.com/watch?v=FN4wGLBiAOQ | 逐步动画演示 Quickselect 的执行过程,适合初学者 |
| NeetCode | Quick Select | https://www.youtube.com/watch?v=XEmy13g1Qxc | LeetCode 实战视角,含代码实现 |
| WilliamFiset | Quickselect | https://www.youtube.com/watch?v=u9N_aPCQDE4 | 算法系列教程,含复杂度分析 |
| CS Dojo | Quickselect in Python | https://www.youtube.com/watch?v=A3Z8Lz8mxgo | Python 实现,适合编程练习 |
教材原文
CLRS 第4版 9.2节原文
The general selection problem—finding the th order statistic for any value of —appears more difficult than the simple problem of finding a minimum. Yet, surprisingly, the asymptotic running time for both problems is the same: . This section presents a divide-and-conquer algorithm for the selection problem. The algorithm RANDOMIZED-SELECT is modeled after the quicksort algorithm of Chapter 7. Like quicksort it partitions the input array recursively. But unlike quicksort, which recursively processes both sides of the partition, RANDOMIZED-SELECT works on only one side of the partition. This difference shows up in the analysis: whereas quicksort has an expected running time of , the expected running time of RANDOMIZED-SELECT is , assuming that the elements are distinct.
参见Wiki
- Quickselect — 期望线性时间选择算法