引言

快速排序(Quicksort)由 Tony Hoare 于 1959 年提出,是最经典的分治算法之一。其核心思想可以用一句话概括:

选择一个 pivot,将数组划分为"小于 pivot"和"大于 pivot"两部分,然后递归处理子数组。

这篇文章将从数学原理出发,结合 Python 实现,逐步深入工程中的优化技巧。

数学基础:渐进复杂度分析

快速排序的时间复杂度依赖于划分是否均匀。设 $T(n)$ 为对长度为 $n$ 的数组排序的期望时间,则有递推式:

$$ T(n) = \frac{1}{n} \sum_{k=0}^{n-1} \big[T(k) + T(n-k-1)\big] + \Theta(n) $$

解此递推式可得期望时间复杂度为 $\Theta(n \log n)$。我们可以用积分近似来验证:

$$ \int_{1}^{n} x \ln x , dx \approx \frac{n^2 \ln n}{2} - \frac{n^2}{4} $$

而最坏情况下,每次划分都极不均匀(例如已排序数组且总是选首元素为 pivot),退化至:

$$ T(n) = T(n-1) + \Theta(n) \implies T(n) = \Theta(n^2) $$

空间复杂度

递归调用栈的深度在最好情况下为 $O(\log n)$,最坏情况下为 $O(n)$。通过尾递归优化可以将最坏空间降至 $O(\log n)$。

算法实现

基础版本:Lomuto 划分

 1def quicksort_lomuto(arr: list[int], low: int, high: int) -> None:
 2    """Lomuto partition scheme — 简单直观但交换次数较多"""
 3    if low >= high:
 4        return
 5
 6    pivot = arr[high]           # 选最后一个元素作为 pivot
 7    i = low - 1                 # i 指向"小于 pivot 的区域"的末尾
 8
 9    for j in range(low, high):
10        if arr[j] < pivot:
11            i += 1
12            arr[i], arr[j] = arr[j], arr[i]
13
14    i += 1
15    arr[i], arr[high] = arr[high], arr[i]  # 将 pivot 放到正确位置
16
17    quicksort_lomuto(arr, low, i - 1)
18    quicksort_lomuto(arr, i + 1, high)

在 Python 中,还可以用更简洁的列表推导式:

1def quicksort_pythonic(arr: list[int]) -> list[int]:
2    """Functional-style quicksort — NOT in-place"""
3    if len(arr) <= 1:
4        return arr
5    pivot = arr[len(arr) // 2]
6    left  = [x for x in arr if x < pivot]
7    mid   = [x for x in arr if x == pivot]
8    right = [x for x in arr if x > pivot]
9    return quicksort_pythonic(left) + mid + quicksort_pythonic(right)

工程优化版本:三路划分 + 插入排序混合

 1// 三路划分 (Three-way Partition) —— 适合大量重复元素
 2void quicksort_3way(int arr[], int low, int high) {
 3    if (low >= high) return;
 4
 5    // 小数组切换到插入排序
 6    if (high - low < 16) {
 7        insertion_sort(arr + low, high - low + 1);
 8        return;
 9    }
10
11    int lt = low, gt = high;
12    int pivot = arr[low];
13    int i = low;
14
15    while (i <= gt) {
16        if (arr[i] < pivot)      swap(arr + lt++, arr + i++);
17        else if (arr[i] > pivot) swap(arr + i, arr + gt--);
18        else                     i++;
19    }
20
21    quicksort_3way(arr, low, lt - 1);
22    quicksort_3way(arr, gt + 1, high);
23}

不同划分策略对比

策略pivot 选择最坏时间额外空间重复元素处理
Lomuto固定末尾$O(n^2)$$O(1)$
Hoare固定中间$O(n^2)$$O(1)$一般
三路划分固定首元素$O(n^2)$$O(1)$优秀
IntroSort三数取中$O(n \log n)$$O(\log n)$良好
PDQSort多种启发式$O(n \log n)$$O(\log n)$极佳

备注:Rust 标准库的 slice::sort_unstable 使用 PDQSort(Pattern-Defeating Quicksort),它在检测到"过于有序"时会自动切换为堆排序。

性能基准测试

使用 Gotesting 包对一百万条随机记录进行基准测试:

 1func BenchmarkQuicksort(b *testing.B) {
 2    data := make([]int, 1_000_000)
 3    for i := range data {
 4        data[i] = rand.Intn(1_000_000)
 5    }
 6    b.ResetTimer()
 7    for i := 0; i < b.N; i++ {
 8        copy := append([]int(nil), data...)
 9        quicksort(copy)
10    }
11}
12// BenchmarkQuicksort-16    4    286 ms/op

快速排序示意图

图:快速排序动画演示 (来源: Wikipedia)

实际应用场景

下表总结了不同语言/库中快速排序的使用情况:

语言 / 库排序函数底层算法是否稳定
C++ std::sortstd::sortIntrosort
Rustslice::sort_unstablePDQSort
Pythonlist.sort()Timsort (归并 + 插入)
Java Arrays.sort (基本类型)Dual-Pivot Quicksort双轴快排
Gosort.SlicePDQSort (≥1.21)
JavaScript V8Array.prototype.sortTimsort

小结

在这篇文章中,我们涵盖了:

  1. 快速排序的递推式 $T(n)$ 及其数学分析
  2. 三种不同风格的代码实现:LomutoPythonic三路划分 C
  3. 不同 pivot 选择策略的性能对比
  4. 各主流语言标准库的排序算法选择

关键结论:快速排序仍是大多数标准库的首选通用排序,但几乎都加入了混合策略来避免最坏情况。


本文共计约 2000 字,适合有一定算法基础的读者。如有疑问欢迎在评论区讨论。