算法基础(十四)—— 随机化快速排序为什么平均表现很好

1. 定位导航

前面已经学习了随机算法的基本思想:

text 复制代码
随机性不是碰运气,而是用来降低坏情况稳定发生的概率。

快速排序正是随机化思想非常典型的应用。

它本身是分治算法:

text 复制代码
划分数组 → 递归处理左右部分 → 得到有序结果

随机化的地方在于:

text 复制代码
每次划分前,随机选择主元。

2. 概念术语

术语 定义 举例
主元 用来划分数组的元素 pivot = 6
划分 把数组分成小于主元和大于主元的部分 [1,2,3] + [6] + [9,8,7]
随机主元 从当前区间随机选择主元 随机选下标
均衡划分 左右子数组规模接近 一半一半
不均衡划分 一边很大,一边很小 0 和 n-1
期望时间 多次随机运行的平均时间 平均 O(n log n)
原地排序 主要在原数组上操作 经典快速排序可原地实现

关键澄清:

  1. 随机化快速排序最终结果仍然正确。
  2. 随机的是执行路径,不是排序结果。
  3. 随机主元不是保证每次最好,而是降低连续最差的概率。
  4. 快速排序平均表现好,但最坏情况仍可能是 O(n²)

3. 快速排序的核心流程

快速排序可以分成四步:

核心逻辑是:

text 复制代码
1. 选择一个主元 pivot
2. 把小于 pivot 的元素放左边
3. 把大于 pivot 的元素放右边
4. 递归排序左右两边

当左右子数组都变成有序后,整个数组自然有序。

4. 为什么固定主元可能很危险

如果每次都固定选择第一个元素作为主元,那么某些输入会非常不友好。

例如输入已经有序:

text 复制代码
[1, 2, 3, 4, 5, 6, 7]

如果每次都选第一个元素,划分可能变成:

text 复制代码
左边为空,右边有 n-1 个元素

这种划分非常不均衡。

递归树会接近一条链,而不是一棵平衡树。

如果每一层都只减少一个元素,递归深度会接近 n,总成本可能退化到:

O(n2) O(n^2) O(n2)

5. 随机化快速排序怎么做

随机化快速排序只改一个关键点:

text 复制代码
主元不固定选,而是随机选。

例如当前数组是:

text 复制代码
[9, 1, 8, 2, 7, 3, 6]

随机选到主元 6 后,可以划分为:

得到:

text 复制代码
小于 6:[1, 2, 3]
主元 6:[6]
大于 6:[9, 8, 7]

接下来只需要递归处理左右两边。

6. 动态推演:随机化快速排序过程

下面用动态图看完整过程。

过程可以概括为:

  1. 当前数组准备划分;
  2. 随机选择一个主元;
  3. 根据主元划分左右两边;
  4. 递归处理左侧;
  5. 递归处理右侧;
  6. 合并得到最终有序结果。

这里的"合并"不像归并排序那样需要额外线性合并,因为划分后主元已经在正确位置,左右两边分别排好后整体自然有序。

7. 为什么平均表现很好

快速排序好不好,关键看划分是否均衡。

如果每次都接近一半一半,递归树高度大约是:

O(logn) O(\\log n) O(logn)

每层划分总成本大约是:

O(n) O(n) O(n)

所以总成本接近:

O(nlogn) O(n \\log n) O(nlogn)

随机化的意义在于:

text 复制代码
不保证每次都均衡,但连续极端不均衡的概率很低。

因此,随机化快速排序通常关注期望运行时间:

O(nlogn) O(n \\log n) O(nlogn)

8. 复杂度分析

快速排序常见复杂度可以这样记:

情况 说明 复杂度
最好情况 每次划分都很均衡 O(n log n)
平均情况 随机主元下的期望表现 O(n log n)
最坏情况 每次都极端不均衡 O(n²)
空间复杂度 递归栈深度 平均 O(log n),最坏 O(n)

要注意:

text 复制代码
随机化并没有消除最坏情况,只是让最坏情况不容易稳定发生。

这就是随机化快速排序的核心价值。

9. 代码实践

下面给出一个直观版本的随机化快速排序。

python 复制代码
import random


def randomized_quick_sort(nums):
    if len(nums) <= 1:
        return nums[:]

    pivot = random.choice(nums)

    less = []
    equal = []
    greater = []

    for x in nums:
        if x < pivot:
            less.append(x)
        elif x > pivot:
            greater.append(x)
        else:
            equal.append(x)

    return randomized_quick_sort(less) + equal + randomized_quick_sort(greater)


if __name__ == "__main__":
    nums = [9, 1, 8, 2, 7, 3, 6]
    print(randomized_quick_sort(nums))

输出:

text 复制代码
[1, 2, 3, 6, 7, 8, 9]

上面这个版本更容易理解,但不是原地实现。

如果追求空间效率,可以使用原地 partition,但代码会更复杂。

原地版本示例

python 复制代码
import random


def partition(arr, left, right):
    pivot_index = random.randint(left, right)
    arr[pivot_index], arr[right] = arr[right], arr[pivot_index]
    pivot = arr[right]

    i = left - 1
    for j in range(left, right):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]

    arr[i + 1], arr[right] = arr[right], arr[i + 1]
    return i + 1


def quick_sort(arr, left, right):
    if left < right:
        pivot_pos = partition(arr, left, right)
        quick_sort(arr, left, pivot_pos - 1)
        quick_sort(arr, pivot_pos + 1, right)


nums = [9, 1, 8, 2, 7, 3, 6]
quick_sort(nums, 0, len(nums) - 1)
print(nums)

10. 常见误区

误区一:随机化后就没有最坏情况

不对。

最坏情况仍然存在,只是发生概率被降低了。

误区二:随机主元一定比固定主元快

不一定。

单次运行可能更快,也可能更慢。随机化关注的是长期平均表现和抗坏输入能力。

误区三:快速排序一定稳定

不是。

经典快速排序通常不是稳定排序。如果相等元素的相对顺序需要保持,需要使用其他策略。

误区四:递归实现不用考虑栈深度

不对。

最坏情况下递归深度可能达到 O(n),工程实现中常会做尾递归优化、深度限制或切换到其他排序。

11. 现代延伸

工程中的排序实现,往往不是单一算法,而是混合策略。

常见做法包括:

优化策略 作用
随机主元 降低坏输入稳定触发最坏情况的概率
三数取中 更稳健地选择主元
小数组切换插入排序 降低常数成本
递归深度限制 防止栈过深
退化时切换堆排序 避免最坏情况不可控

很多高性能排序实现都会综合这些策略。

这说明一个重要事实:

text 复制代码
理论复杂度很重要,但工程实现还要考虑常数、内存、递归深度和坏输入防护。

12. 思考题

  1. 快速排序为什么属于分治算法?
  2. 为什么固定选择第一个元素作为主元可能很危险?
  3. 随机主元为什么能降低坏情况稳定发生的概率?
  4. 随机化快速排序的平均复杂度是多少?最坏复杂度是多少?
  5. 为什么工程排序实现常常会混合多种排序算法?

13. 本篇小结

本篇讲清楚了随机化快速排序。

核心结论是:

  • 快速排序通过主元划分数组,再递归排序左右两边;
  • 主元选择会影响划分是否均衡;
  • 固定主元可能被特殊输入触发最坏情况;
  • 随机主元可以降低连续坏划分发生的概率;
  • 随机化快速排序平均运行时间通常是 O(n log n)
  • 最坏情况仍然可能是 O(n²)
  • 工程实现中常结合随机化、插入排序、深度限制等策略。

一句话总结:

text 复制代码
随机化快速排序不是保证每次都最好,而是避免总是最坏。
相关推荐
吴可可1231 小时前
Teigha中OdGe几何库详解及C#使用
算法
爱喝水的鱼丶1 小时前
SAP-ABAP:变量、常量、结构与内表声明(10篇博客合集) 第六篇:ABAP 7.40+新特性:声明语法的简化写法与兼容注意事项
运维·服务器·开发语言·学习·算法·sap·abap
国科安芯2 小时前
AS32S601商业航天级抗辐照MCU芯片:架构设计与技术特性研究
单片机·嵌入式硬件·算法·安全·架构·risc-v
菜菜的顾清寒2 小时前
力扣HOT100(34)图论-岛屿数量
算法·leetcode·图论
名字不好奇2 小时前
大模型的思考模式:它真的在“想“吗?
人工智能·算法
Run_Teenage2 小时前
算法模板:输入输出,并查集
java·开发语言·算法
高一学习c++会秃头吗2 小时前
操作系统内存块分配算法
算法
洛水水2 小时前
【力扣100题】57.合并区间
算法·leetcode
玉树临风ives2 小时前
atcoder ABC 458 题解
数据结构·c++·算法