找第 k 小的数(快速选择,分治算法)
一、自然语言描述
算法思路借鉴快速排序,采用分治思想:
- 在当前待处理区间里,任选一个元素作为基准 pivot。
- 分区(partition):把区间内小于 pivot 的元素放到 pivot 左边,大于 pivot 的放到右边。分区完成后,pivot 就处在它在有序数组里的正确位置。
- 假设基准最终下标为
pos:- 如果
pos == k-1(数组从 0 开始):基准就是第 k 小元素,直接返回。 - 如果
pos > k-1:第 k 小在左半区间,递归在左区间查找。 - 如果
pos < k-1:第 k 小在右半区间,递归在右区间查找,并且更新 k,在右区间找第k-pos-1小。
- 如果
伪代码如下:
// a:数组,l左边界,r右边界,k:要找第k小(从1开始)
QuickSelect(a, l, r, k):
if l == r:
return a[l]
pos = Partition(a, l, r) // 分区,返回pivot位置
lenLeft = pos - l + 1 // 左段元素个数(含pivot)
if lenLeft == k:
return a[pos]
else if k < lenLeft:
return QuickSelect(a, l, pos-1, k) // 在左边找第k小
else:
return QuickSelect(a, pos+1, r, k - lenLeft) //右边找
Partition 伪代码:
Partition(a,l,r):
pivot = a[l] //选最左元素作为基准
i = l, j = r
while i<j:
while i<j and a[j] >= pivot: j--
a[i] = a[j]
while i<j and a[i] <= pivot: i++
a[j] = a[i]
a[i] = pivot
return i
二、时间复杂度分析
- 最好时间复杂度 (O(n))
每次分区的基准刚好把数组切分成大致均等的两部分。每次递归只处理其中一半。
总代价:(n + n/2 + n/4 + \dots = O(n))。 - 最坏时间复杂度 (O(n^2))
每次选到的 pivot 都是当前区间的最大值或最小值,每次分区只能去掉 1 个元素。
递归总代价:(n+(n-1)+(n-2)+\dots=O(n^2))。
如果采用随机选取 pivot,期望时间复杂度可以稳定为(O(n))。
三、对分治法的体会与思考(作业正文)
分治法的核心思想是分、治、合:将一个规模较大的原问题,拆成若干规模更小、结构相同的子问题;递归求解各个子问题;最后把子问题的解合并,得到原问题答案。
在快速选择找第 k 小这个例子里,可以看到分治和普通排序不一样:快速选择不需要把整个数组全部排好序,只需要求解有用的那一侧子问题,另一半直接舍弃,这就是分治带来的优势,减少了不必要的计算。
分治法适用的问题一般具备几个特点:问题可以拆分、子问题独立、子问题解可以合并。但分治法不是万能的,如果划分极度不平衡,就会出现很高的最坏复杂度,就像快速选择每次选到极差基准时,复杂度退化到(O(n^2))。这提醒我们,划分策略会极大影响算法效率。
对比前面矩阵链、挖地雷这类动态规划问题,分治法处理的是相互独立的子问题,而动态规划的子问题大量重叠。分治适合直接递归拆分,动态规划则要保存子问题答案避免重复计算。
通过本章学习我认识到,设计分治算法的重点,不仅是写出递归公式,更要设计高效的划分方案,尽量平衡子问题规模,降低整体时间开销。

浙公网安备 33010602011771号