找第 k 小的数(快速选择,分治算法)

一、自然语言描述

算法思路借鉴快速排序,采用分治思想:

  1. 在当前待处理区间里,任选一个元素作为基准 pivot。
  2. 分区(partition):把区间内小于 pivot 的元素放到 pivot 左边,大于 pivot 的放到右边。分区完成后,pivot 就处在它在有序数组里的正确位置。
  3. 假设基准最终下标为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))。这提醒我们,划分策略会极大影响算法效率。

对比前面矩阵链、挖地雷这类动态规划问题,分治法处理的是相互独立的子问题,而动态规划的子问题大量重叠。分治适合直接递归拆分,动态规划则要保存子问题答案避免重复计算。
通过本章学习我认识到,设计分治算法的重点,不仅是写出递归公式,更要设计高效的划分方案,尽量平衡子问题规模,降低整体时间开销。

posted @ 2026-10-13 23:45  小米粥米小  阅读(5)  评论(0)    收藏  举报