2.2 线性时间选择算法
这一篇讲一个会让你重新认识"分治"威力的算法。问题很简单:给 个数,找出第 小的那个。(中位数就是 的特例。)
最朴素的解法是先排序再取第 个,。但我们要做的是——不排序, 搞定。 这听起来不可思议:找第 小居然能比排序还快一整个 因子?能。这就是 Blum、Floyd、Pratt、Rivest、Tarjan 五人在 1973 年给出的线性时间选择算法(常被叫做 "Median-of-Medians" 中位数的中位数法)。它也是分治思想最精彩的运用之一。
1.关键想法:划分,然后只递归一边
回忆快排的 PARTITION(2.1):选个基准,把数组划成"小左大右",基准归位到某个位置 。如果基准归位到了第 个位置,那它就是第 小。
选择算法的洞见:我们不需要把两边都排好。归位后,如果 ,基准就是答案,完事;如果 ,答案只在左半边,只递归左半边;如果 ,只递归右半边。每次只走一边,不像快排要走两边。
RANDOMIZED-SELECT(A, p, r, k) // 找 A[p..r] 里第 k 小
if p == r: return A[p]
q = RANDOMIZED-PARTITION(A, p, r)
i = q - p + 1 // 基准是当前段第 i 小
if k == i: return A[q]
elseif k < i: return RANDOMIZED-SELECT(A, p, q-1, k)
else: return RANDOMIZED-SELECT(A, q+1, r, k-i)
如果用随机选基准,期望复杂度是 ——因为每次只递归一边,期望代价是个等比递减的级数 。这已经够实用了。
但期望 不满足我们。我们要的是最坏情况 ——不管输入多刁钻、随机数多倒霉,都保证线性。这就需要一个更聪明的选基准方法。
2.中位数的中位数:保证基准"不太偏"
随机选基准,期望很好,但运气差时会偏。我们要找一个基准,保证它既不会太小也不会太大——具体说,保证它至少比 的元素大、又至少比 的元素小。这样每次划分后,至少能甩掉 的元素,递归规模保证缩小。
怎么找这样的基准?这就是"中位数的中位数":
- 把 个元素每 5 个一组,分成 组。
- 找出每组(5 个元素)的中位数——5 个数找中位数用插入排序取中间那个即可,。
- 这样得到 个中位数。递归地用选择算法找出这些中位数的中位数 (这是基准)。
就是我们要的"不太偏"的基准。为什么?看图:
每组5个,取中位数(每组第3大):
组1: [· · x₁ · ·] 组2: [· · x₂ · ·] ... 共 ⌈n/5⌉ 组
- 是这 个中位数的中位数,所以至少有一半的中位数 。
- 每个中位数 的那组里,至少有 3 个元素(中位数本身及它右边两个)。
- 所以至少有 个元素 。对称地,约 个元素 。
于是无论怎么划分,至少能甩掉 的元素,递归规模最多是 。
3.复杂度:
写出递归关系。算法做了三件事:
- 每组找中位数、其他扫一遍:。
- 递归找中位数的中位数:规模 ,代价 。
- 用 划分后,递归处理较大的那一边:规模最多 ,代价 。
关键:。两个递归子问题的规模加起来只占 ,剩下的 用来支付 的工作。用递归树(4.1 节)或代入法可证:每层总代价是个等比递减级数 。所以 。
这就是最坏情况线性的来历。 这个不等式是整个证明的命门——子问题规模的总占比严格小于 1,保证了代价收敛。
4.为什么 5:理论与实践的权衡
你可能会问:为什么每组分 5 个?分 3 个、分 7 个不行吗?
这是理论和实践的权衡。如果每组 个,递归关系是 。要让总占比 保证线性, 不能太小:
- :中位数的中位数至少比约 大、比约 小,递归规模上界太大, 可能退到 。
- :刚好让 ,线性成立。这是能保证线性的最小分组大小,常数也还可控。
- :也线性,但常数更大、实现更繁琐,没有额外好处。
所以 5 是理论上的"甜蜜点":最小且够用。这个数字背后是有道理的,不是随便选的。
5.这个算法的理论意义
这个算法最震撼的地方,不是它多实用(实际上随机化选择的期望 常数更小、工程上更爱用),而是它证明了一件事:选择问题本质上是线性的,比排序便宜。
排序有 的下界(1.8 节提过,4.9 节会证),所以排序再取第 个是 。但选择问题不需要全序信息,只需要"第 个在哪",所以它能做到 。这两个问题的难度差了一个 ,而线性选择算法把这个差距坐实了。 这种"精确刻画一个问题到底有多难"正是算法分析(第四卷)的追求。
它还是分治的范本:每次只递归一边(不像快排两边都走)、靠精心设计的基准保证子问题规模可控。第二卷结尾 2.7 会把这套"保证收缩比例"上升为分治设计的一般法则。
6.练习
Q1. 选择算法为什么能比排序()更快地找到第 小?
因为它不需要排好整个序。排序要确定所有元素的相对顺序,有 下界;而选择只要定位"第 个在哪",不需要全序信息。划基准归位后,只需判断 在左还是右、只递归一边,省掉了另一边的全部工作。这种"只走一边"让它摆脱了排序的下界,做到 。
Q2. 为什么"中位数的中位数"能保证基准至少比 的元素大?写出这个 是怎么来的。
是 个组中位数的中位数,故至少一半组中位数 。每个这样的组里至少 3 个元素(中位数及右两个)。所以 的元素至少 个。对称地 的也约 。于是划分后至少甩掉 ,递归规模 。
Q3.(思考题) 线性选择的最坏 和随机化选择的期望 ,工程上一般用哪个?为什么?
一般用随机化选择(期望 )。原因:中位数的中位数法虽然最坏线性,但常数大(要分组、递归找中位数的中位数),实现复杂;而随机化选基准期望也是 、常数小、实现简单,最坏虽然理论上是 但随机化后实际几乎不出现。和快排一样的道理——工程更看重期望性能和常数,而不是理论最坏界。
7.小结
线性时间选择算法用"中位数的中位数"挑出一个不太偏的基准,保证每次划分至少甩掉 的元素,从而把选择问题做到最坏 。 是线性成立的命门。它的理论意义在于坐实了"选择比排序便宜一整个 "。下一篇我们把分治带到几何世界——最近点对问题。