最小值和最大值
在一个有 n 个元素的集合中,需要做多少次比较才能确定其最小元素呢?
最简单的方式:依次遍历集合中的每个元素,并记录下当前最小元素。这样会经过 n-1 次比较。进一步,如果我们希望同时找到最小值和最大值,如此方法则需要 2n-2 次比较。
但事实上,我们有更快速的方法,能同时找到最大值和最小值,且只需要 3[n/2] 次比较。
步骤如下:
- 将输入元素成对处理。对于每一对元素 a 和 b,相互比较。
- 将 a、b 中的较小者与当前最小值比较,将较大者与当前最大值比较。这样,每对元素需要3次比较。
在此基础上,比较次数可以进一步缩减到 **O(logn)**。
步骤如下:
- 递归处理:每次能将最大值、最小值所在集合的范围缩小一半。
1 | def find_min_max(arr): |
选择算法
在一个有 n 个元素的集合中,如何找到第 i 小的元素呢?
期望为线性时间的选择算法
RANDOMIZED-SELECT
是一个分治算法,用于在数组中找到第 i
小的元素。这个算法是基于快速排序的分区操作 RANDOMIZED-PARTITION
的,但它只递归地处理分区的一边,而不是两边。这使得 RANDOMIZED-SELECT
的期望运行时间为 **O(n)**,这比快速排序的 O(n · log n) 要好。
步骤如下:
- 边界检查:如果子数组的范围
p
和r
相等,说明已经找到了第i
小的元素,直接返回。 - 随机分区:使用
RANDOMIZED-PARTITION
对数组A[p..r]
进行分区,返回分区的基准索引q
。 - 计算基准位置:计算枢轴元素相对于子数组起始位置
p
的位置k
。 - **比较
i
和k
**:如果i
等于k
,则枢轴元素就是第i
小的元素,返回A[q]
。如果i
小于k
,则在左子数组A[p..q-1]
中递归寻找第i
小的元素。否则,在右子数组A[q+1..r]
中递归寻找第i-k
小的元素。
伪代码如下:
1 | RANDOMIZED-SELECT (A, p, r, i) |
最坏情况为线性时间的选择算法
因为随机选可能很倒霉
我们给出一个选择算法 SELECT
,最坏情况仍为线性时间。
步骤如下:
- 分组:将数组分为大约
n/5
组,每组包含5个元素。如果有剩余元素(即n % 5
不为0),则形成最后一组。 - 寻找每组的中位数:对每组进行插入排序(或其他线性时间排序),然后选择每组的中位数。
- 递归寻找中位数的中位数:对所有组的中位数进行递归调用
SELECT
算法,找到它们的中位数x
。 - 划分数组:使用
x
作为主元,X
是区间里第k
小的元素,对整个数组进行划分。划分后,数组被分为低区(元素小于x
)、中区(元素等于x
)和高区(元素大于x
)。 - 递归选择:根据
i
的值,决定是在低区、中区还是高区递归调用SELECT
算法:- 如果
i
等于k
(x
在数组中的位置),则返回x
。 - 如果
i
小于k
,则在低区递归调用SELECT
。 - 如果
i
大于k
,则在高区递归调用SELECT
,且i = i-k
。
- 如果
图片
我们下面分析该算法的运行时间,证明:其在最坏情况下,运行时间仍为 **O(n)**。
证明