分位统计量

分位统计量

第K大的数

在许多算法与数据分析任务中,我们不仅需要排序整个数组,还需要找出某个特定顺序统计量,例如: • 一个数组中第 K 大的元素; • 所有元素的中位数(K = n/2); • 某个分位点(25%、75%等)…
最直接的办法是先对数组进行排序,然后取第 K 个位置的元素。这种方法的时间复杂度为 O(n \log n),因为我们需要对所有元素进行排序。
但我们真正关心的,只是一个位置的元素 —— 我们是否有更快的方法?

快速选择(QuickSelect)

快排中的“轴枢”启发 我们在快速排序中,每次选一个枢轴 pivot,通过划分操作把数组分成两部分: • 左边元素 <= pivot; • 右边元素 >= pivot。 最终 pivot 会被放置固定位置上。
可见:

若 pivot 恰好是我们要找的第 K 大元素,就不必继续递归。


因此可得快速选择的具体做法,先令target等于N-K,即找第N-K+1小的数,其下标为target: 1. 随机选一个枢轴; 2. 利用快速排序的 partition 划分函数,找出 pivot 的位置 idx; 3. 比较该位置与目标索引: • 若 idx == target,返回 pivot; • 若 idx > target,在左半边递归查找; • 否则在右半边递归查找。
理解做法并分析时间复杂度。 ### 快速选择的实现

实现一个 quickselect(arr, k) 函数,返回第 K 大的元素 (也就是增序列下标N-K位置的数)

import random

def partition(arr, low, high):
			"""
			对arr序列从low到high下标,找到一个枢轴,并返回其下标。(在arr中,比枢轴小的数都在idx左边,大的在右边)
			"""

def quickselect(arr, k):
    """
    快速选择第K大元素(转换为第n-k小)
    :param arr: 输入数组
    :param k: 要找的第K大元素
    :return: 该元素值
    """
    n = len(arr)
    target = n - k  # 第k大转为第n-k小(序列下标)
    low, high = 0, n - 1

    while low <= high:
        idx = partition(arr, low, high)
        if idx == target:
            return arr[idx]
        elif idx < target:
            low = idx + 1
        else:
            high = idx - 1

if __name__ == "__main__":
    #测试1:随机数组
    random.seed(42)  # 设置随机种子,确保结果可重现
    arr1 = [random.randint(1, 100) for _ in range(10)]
    k1 = 3
    result1 = quickselect(arr1.copy(), k1)  # 使用副本避免修改原数组
    sorted_arr1 = sorted(arr1)
    expected1 = sorted_arr1[-k1]
    print(f"测试1 - 随机数组:")
    print(f"原数组: {arr1}")
    print(f"第{k1}大元素: {result1}, 预期值: {expected1}")
    print(f"测试{'通过' if result1 == expected1 else '失败'}\n")

    #测试2:包含重复元素的数组
    arr2 = [5, 3, 8, 8, 1, 5, 9, 3, 5]
    k2 = 2
    result2 = quickselect(arr2.copy(), k2)
    sorted_arr2 = sorted(arr2)
    expected2 = sorted_arr2[-k2]
    print(f"测试2 - 包含重复元素:")
    print(f"原数组: {arr2}")
    print(f"第{k2}大元素: {result2}, 预期值: {expected2}")
    print(f"测试{'通过' if result2 == expected2 else '失败'}\n")

    #测试3:边界条件 - k=1(最大元素)
    arr3 = [10, 20, 30, 40, 50]
    k3 = 1
    result3 = quickselect(arr3.copy(), k3)
    sorted_arr3 = sorted(arr3)
    expected3 = sorted_arr3[-k3]
    print(f"测试3 - 最大元素:")
    print(f"原数组: {arr3}")
    print(f"第{k3}大元素: {result3}, 预期值: {expected3}")
    print(f"测试{'通过' if result3 == expected3 else '失败'}\n")

BFPRT 算法:中位数的中位数算法

快速选择的平均复杂度为 O(n),但可能退化为 O(n^2)

BFPRT分治法

BFPRT算法和二分查找中对“矩阵”进行分治的做法很像,也是通过矩阵的中心数,来排除一部分数据。 image
我们通过以下几个步骤确保选出的“轴枢”可以高概率地“削减”足够多的元素,从而保证递归过程每次至少减少 30% 左右的输入规模:

  1. 将原始数组划分为若干个 5 个元素的组 请看图中所有的竖列(粉红色背景小矩形):每个小矩形内有 5 个点,这就是我们将原始数据分成的小组。 组的个数 ≈ n / 5(设 n 是总元素数量) 不足 5 个的组,可以用一个全局最小值(如 -1)补全。
  2. 对每组内部排序,取出中位数 图中每个竖列的小组中,绿色圆点表示每组的元素。中间处于红框中的数为每组中位数。 每列上两个数小于中位数,下两个大于。
  3. 将所有组的中位数组成新数组,对其递归调用本算法找中位数。 图中红色矩形框住的内容,正是“所有组中位数”的集合。这些点来自所有小组的中位点,组成新的长度为 n/5 的数组。 对这个新数组再次使用本算法,递归找出它的中位数,作为我们最终要用的“枢轴”。即图中棕色点。
  4. 这个中位数(棕色圆点)就是我们分治的“枢轴” 有如下重要性质: a. 棕点及左上角(图中蓝框的点)均小于等于枢轴 b. 棕点及右下角(图中黑框的点)均大于等于枢轴
  5. 进行排除,分解,先令x=n-k+1;找第k大数就是找第x小数 a. 若x>蓝框数数量,则在非蓝框数中找第k大 b. 若k>黑框数数量,则在非黑框数中找第(k-黑框数数量)大 c. 当k=1或n'(n'是当前子问题的数数量),或n'<=5,均可在不大于O(n')的用时完成找数。 可以证明在c不成立的情况下,a,b两点至少成立一个。

与AI教师讨论每一步在做的事和最终想要实现的效果。可以联想你之前学习到的二分查找中的矩阵分治。

BFPRT 复杂度分析

在理解了“中位数的中位数”算法的流程之后,我们接下来要正式分析它的时间复杂度,并证明该算法的整体运行时间为 O(n)


写出递推式

根据 BFPRT 算法的执行过程,依次与AI教师讨论每步的时间复杂度:

  1. n 个元素分组、找出每组中位数
  2. 递归地找所有中位数的中位数(枢轴)
  3. 额外一次线性 partition
  4. 将原始数组按枢轴划分,并在一侧继续递归

image

从而得到最终的递推式。


证明 T(n) = O(n)

这里的证明并不复杂,使用高中学习的数学归纳法即可。 提示:先假设满足T(n)<=cn;然后证明递推式<=d*cn<=cn,其中d小于1。

与AI教师探讨证明流程。