排序系列第 5 篇聚焦快速排序:平均 O(n log n)、原地、常数低,但需通过枢轴策略与尾递归优化来规避最坏 O(n^2) 与栈深问题。本文从 ACERS 角度给出理论到工程落地的全景。
目标读者
- 想把快排写到“工程可用”水平的开发者。
- 对枢轴选择、重复元素处理、尾递归/混合策略有疑问的同学。
- 需要理解 std::sort / Introsort 设计动机的人。
背景/动机
- 快排因原地、缓存友好、常数低而常为首选,但最坏 O(n^2) 与重复元素性能需谨慎。
- 工程实践通过随机枢轴、三数取中、三路划分、尾递归和小分段插排来提升稳健性。
A — Algorithm(题目与算法)
主题:在保持原地、低常数的前提下,实现平均 O(n log n)、抗退化的快速排序。
基础示例
数组 [3, 5, 2, 2, 8],枢轴=3:
- 分区后 →
[2,2,3,5,8],左侧小于 3,右侧大于等于 3。 - 递归处理左右子数组。
C — Concepts(核心思想)
| 关键概念 | 说明 |
|---|---|
| 枢轴选择 | 随机枢轴、三数取中(首/中/尾取中)、五数取中等,减少退化概率。 |
| 分区策略 | Lomuto(单边)简单但交换多;Hoare(双边)交换少;三路划分适合重复多。 |
| 重复元素 | 三路划分(<,=,>) 避免大量重复时退化。 |
| 尾递归优化 | 始终递归较小段,对较大段用循环,控制栈深 O(log n)。 |
| 混合策略 | 子数组小于阈值切换插排;递归深度过大切换堆排(Introsort 思想)。 |
复杂度
- 平均时间 O(n log n),最坏 O(n^2)(当枢轴极端不平衡)。
- 空间:递归栈 O(log n) 平均,最坏 O(n),可用尾递归优化减轻。
- 不稳定,原地。
E — Engineering(工程应用)
场景 1:通用后端排序(Go)
背景:数据量 1e5,分布随机。
为何:Go 内置 sort.Slice 基于快排/堆排混合;演示改进版带小段插排。
package main
import "fmt"
func insertion(a []int, l, r int) {
for i := l+1; i <= r; i++ {
key := a[i]; j := i-1
for j >= l && a[j] > key { a[j+1]=a[j]; j-- }
a[j+1]=key
}
}
func partition(a []int, l, r int) int {
pivot := a[(l+r)>>1]
i, j := l, r
for i <= j {
for a[i] < pivot { i++ }
for a[j] > pivot { j-- }
if i <= j { a[i], a[j] = a[j], a[i]; i++; j-- }
}
return i
}
func quick(a []int, l, r int) {
for r-l+1 > 16 {
p := partition(a, l, r)
if p-l < r-p { quick(a, l, p-1); l = p } else { quick(a, p, r); r = p-1 }
}
insertion(a, l, r)
}
func main(){
arr := []int{3,5,2,2,8,1,7}
quick(arr,0,len(arr)-1)
fmt.Println(arr)
}
场景 2:重复元素多的数组(Python 三路划分)
背景:大量重复值(如分桶后 ID 排序),二路分区容易退化。 为何:三路划分一次性处理 = pivot 的区间。
def quick3(a, l=0, r=None):
if r is None: r = len(a)-1
while l < r:
if r - l + 1 <= 16:
for i in range(l+1, r+1):
key=a[i]; j=i-1
while j>=l and a[j]>key:
a[j+1]=a[j]; j-=1
a[j+1]=key
return
pivot = a[(l+r)//2]
lt, i, gt = l, l, r
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]; lt+=1; i+=1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]; gt-=1
else:
i+=1
if lt-l < r-gt: quick3(a, l, lt-1); l = gt+1
else: quick3(a, gt+1, r); r = lt-1
return a
arr=[3,5,2,2,8,1,7,2,2]
quick3(arr)
print(arr)
场景 3:C++ 性能敏感分区(Hoare + 三数取中)
背景:性能敏感、需低交换、枢轴更稳健。
#include <bits/stdc++.h>
using namespace std;
int median3(vector<int>& a, int l, int r){
int m = l + (r-l)/2;
if(a[m] < a[l]) swap(a[m], a[l]);
if(a[r] < a[l]) swap(a[r], a[l]);
if(a[m] < a[r]) swap(a[m], a[r]); // a[r] = median
return a[r];
}
int partition(vector<int>& a, int l, int r){
int pivot = median3(a,l,r);
int i=l-1, j=r;
while(true){
do{ i++; } while(a[i] < pivot);
do{ j--; } while(a[j] > pivot);
if(i>=j) break;
swap(a[i], a[j]);
}
swap(a[i], a[r]);
return i;
}
void quick(vector<int>& a, int l, int r){
while(l < r){
if(r-l+1 <= 16){
for(int i=l+1;i<=r;++i){int key=a[i], j=i-1; while(j>=l && a[j]>key){a[j+1]=a[j]; j--;} a[j+1]=key;}
return;
}
int p = partition(a,l,r);
if(p-l < r-p){ quick(a,l,p-1); l=p+1; }
else{ quick(a,p+1,r); r=p-1; }
}
}
场景 4:JavaScript 前端小数组优化
背景:中小数组排序,使用三数取中 + 插排阈值。
function insertion(a,l,r){
for(let i=l+1;i<=r;i++){
const key=a[i]; let j=i-1;
while(j>=l && a[j]>key){ a[j+1]=a[j]; j--; }
a[j+1]=key;
}
}
function partition(a,l,r){
const m = l + ((r-l)>>1);
if(a[m]<a[l]) [a[m],a[l]]=[a[l],a[m]];
if(a[r]<a[l]) [a[r],a[l]]=[a[l],a[r]];
if(a[r]<a[m]) [a[r],a[m]]=[a[m],a[r]];
const pivot = a[r];
let i=l-1;
for(let j=l;j<r;j++) if(a[j]<=pivot){ i++; [a[i],a[j]]=[a[j],a[i]]; }
[a[i+1],a[r]]=[a[r],a[i+1]];
return i+1;
}
function quick(a,l=0,r=a.length-1){
while(l<r){
if(r-l+1<=16){ insertion(a,l,r); return; }
const p=partition(a,l,r);
if(p-l < r-p){ quick(a,l,p-1); l=p+1; }
else{ quick(a,p+1,r); r=p-1; }
}
return a;
}
console.log(quick([3,5,2,2,8,1,7]));
R — Reflection(反思与深入)
- 复杂度:平均 O(n log n),最坏 O(n^2);空间为栈深 O(log n) 平均,尾递归 + 小段插排可控。
- 替代方案:
- 需稳定或可预测上界 → 归并 / 堆排序 / TimSort。
- 范围可知 → 计数/桶/基数。
- 标准库选择:C++
std::sort= Introsort(快排+堆排+插排);Python/Java 则是 TimSort(稳定)。
- 为何当前方法可行:
- 随机/三数取中降低退化概率;
- 三路划分解决重复元素;
- 尾递归 + 小段插排降低栈深与常数,贴合工程实践。
S — Summary(总结)
- 快排优势:原地、常数低、缓存友好,平均 O(n log n)。
- 风险点:枢轴极端导致 O(n^2);重复元素多时退化;不稳定。
- 稳健策略:随机/三数取中枢轴,三路划分应对重复,小分段插排,尾递归控制栈,必要时引入 Introsort 思想。
- 选型建议:稳定需求或外部排序用归并/TimSort;内存紧张且随机分布选快排/Introsort;重复多用三路划分。
实践指南 / 步骤
- 选枢轴策略:默认随机或三数取中;性能敏感可加五数取中。
- 重复多则用三路划分;否则二路分区即可。
- 设置小分段阈值(如 16/24),切换插排;设栈深阈值,必要时回退堆排(Introsort)。
- 准备测试集:随机、逆序、全相等、重复多、大数组,检验退化与稳定性风险。
常见问题与注意事项
- Lomuto 分区交换多,Hoare 分区返回索引需注意递归区间。
- 递归深度过深导致栈溢出:用尾递归优化或迭代写法。
- 重复元素未处理好时会导致退化:三路划分是关键。
- 枢轴选择固定取首元素在有序数组上会退化。
可运行示例:多语言最小版
Python(随机枢轴 + 三路)
import random
def quick3(a, l=0, r=None):
if r is None: r = len(a)-1
while l < r:
if r-l+1 <= 16:
for i in range(l+1, r+1):
key=a[i]; j=i-1
while j>=l and a[j]>key:
a[j+1]=a[j]; j-=1
a[j+1]=key
return a
pivot_i = random.randint(l, r)
a[l], a[pivot_i] = a[pivot_i], a[l]
pivot = a[l]
lt, i, gt = l, l+1, r
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]; lt+=1; i+=1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]; gt-=1
else:
i+=1
if lt-l < r-gt: quick3(a, l, lt-1); l = gt+1
else: quick3(a, gt+1, r); r = lt-1
return a
arr=[3,5,2,2,8,1,7,2,2]
quick3(arr); print(arr)
C(Hoare 分区 + 插排阈值)
#include <stdlib.h>
void insertion(int *a,int l,int r){
for(int i=l+1;i<=r;i++){
int key=a[i], j=i-1;
while(j>=l && a[j]>key){ a[j+1]=a[j]; j--; }
a[j+1]=key;
}
}
int partition(int *a,int l,int r){
int pivot=a[(l+r)/2];
int i=l-1, j=r+1;
while(1){
do{ i++; } while(a[i]<pivot);
do{ j--; } while(a[j]>pivot);
if(i>=j) return j;
int t=a[i]; a[i]=a[j]; a[j]=t;
}
}
void quick(int *a,int l,int r){
while(l<r){
if(r-l+1<=16){ insertion(a,l,r); return; }
int p=partition(a,l,r);
if(p-l < r-p){ quick(a,l,p); l=p+1; }
else{ quick(a,p+1,r); r=p; }
}
}
C++(三数取中 + Hoare)
int partition(vector<int>& a,int l,int r){
int m=l+(r-l)/2;
if(a[m]<a[l]) swap(a[m],a[l]);
if(a[r]<a[l]) swap(a[r],a[l]);
if(a[r]<a[m]) swap(a[r],a[m]);
int pivot=a[m];
int i=l-1,j=r+1;
while(true){
do{i++;}while(a[i]<pivot);
do{j--;}while(a[j]>pivot);
if(i>=j) return j;
swap(a[i],a[j]);
}
}
Go(简版二路)
func Quick(a []int, l, r int){
for l<r {
if r-l+1 <= 16 { insertion(a,l,r); return }
p := partition(a,l,r)
if p-l < r-p { Quick(a,l,p-1); l=p }
else { Quick(a,p,r); r=p-1 }
}
}
Rust(三路)
pub fn quick3(a: &mut [i32]) {
fn insertion(a: &mut [i32]) {
for i in 1..a.len() {
let key=a[i]; let mut j=i as i32-1;
while j>=0 && a[j as usize]>key { a[(j+1) as usize]=a[j as usize]; j-=1; }
a[(j+1) as usize]=key;
}
}
fn sort(a: &mut [i32]) {
let n=a.len();
if n<=16 { insertion(a); return; }
let pivot=a[n/2];
let (mut lt, mut i, mut gt) = (0,0,n-1);
while i<=gt {
if a[i]<pivot { a.swap(lt,i); lt+=1; i+=1; }
else if a[i]>pivot { a.swap(i,gt); if gt==0 {break;} gt-=1; }
else { i+=1; }
}
sort(&mut a[..lt]);
sort(&mut a[gt+1..]);
}
if !a.is_empty() { sort(a); }
}
JavaScript(三数取中 + 插排)
function insertion(a,l,r){
for(let i=l+1;i<=r;i++){
const key=a[i]; let j=i-1;
while(j>=l && a[j]>key){ a[j+1]=a[j]; j--; }
a[j+1]=key;
}
}
function quick(a,l=0,r=a.length-1){
while(l<r){
if(r-l+1<=16){ insertion(a,l,r); return a; }
const m=l+((r-l)>>1);
if(a[m]<a[l]) [a[m],a[l]]=[a[l],a[m]];
if(a[r]<a[l]) [a[r],a[l]]=[a[l],a[r]];
if(a[r]<a[m]) [a[r],a[m]]=[a[m],a[r]];
const pivot=a[m];
let i=l, j=r;
while(i<=j){
while(a[i]<pivot) i++;
while(a[j]>pivot) j--;
if(i<=j){ [a[i],a[j]]=[a[j],a[i]]; i++; j--; }
}
if(j-l < r-i){ quick(a,l,j); l=i; }
else { quick(a,i,r); r=j; }
}
return a;
}
console.log(quick([3,5,2,2,8,1,7]));
最佳实践与建议
- 默认使用语言标准库排序;自实现需:随机/三数取中枢轴、三路划分(重复多)、小分段插排、尾递归控制栈。
- 需要稳定时改用归并/TimSort;需要严格上界时考虑 Introsort(快排+堆排)。
- 基准测试覆盖:随机、逆序、全相等、重复多、大规模,观察退化与常数。
小结 / 结论
- 快排以原地、低常数著称,但必须用枢轴策略与三路划分避免退化。
- 尾递归优化 + 插排阈值是工程实现的标配;深度过大可回退堆排(Introsort)。
- 选型遵循:稳定/外部排序 → 归并/TimSort;内存紧张且随机分布 → 快排/Introsort;重复多 → 三路划分。
参考与延伸阅读
- Hoare, “Quicksort” (1961)
- Bentley & McIlroy, “Engineering a Sort Function” (1993)
- C++
std::sort与std::stable_sort源码笔记
元信息
- 阅读时长:约 16 分钟
- SEO 关键词:快速排序, 枢轴选择, 三路划分, 尾递归优化, Introsort
- 元描述:排序专题第五篇,深入讲解快速排序的枢轴策略、三路划分、尾递归与混合优化,附多语言实现与工程选型建议。
行动号召(CTA)
- 用真实数据分布基准测试:随机、逆序、重复多,比较随机枢轴 vs 固定枢轴性能。
- 在你的排序实现中加入“小分段插排 + 尾递归优化”,对比栈深与耗时。
- 关注后续系列:堆排序、非比较排序、TimSort/Introsort、排序选型实战篇。