Skip to content

参考:快速排序 API、复杂度与优化速查

基于通用算法概念 · 核于 2026-07

速查

  • 定义:快排 = 分治。选 pivot → 分区(小左大右)→ 递归两边;pivot 分区后即落最终位。
  • 复杂度:平均 O(n log n)、最坏 O(n²)(有序+取首元素);空间 O(log n) 递归栈(尾递归优化后)。
  • 稳定性不稳定(分区交换打乱相等元素相对序);需稳定用归并/Timsort。
  • Lomuto 分区:单指针 ij 扫到 < pivotswap(a[++i], a[j]),末元素当 pivot,返回 i+1
  • Hoare 分区:双指针相向,左找 ≥pivot、右找 ≤pivot 交换,相遇返回 j;交换少、更对称,递归用 (lo,j)(j+1,hi)
  • 三路分区lt/i/gt 三指针切成 <p/=p/>p,重复元素一次性落位,全相等输入 O(n)。
  • pivot 选择:随机化(期望 O(n log n))/ 三数取中(首中末的中位数,零开销规避有序退化)。
  • 优化清单:小数组切插入(阈值 16~47)/ 尾递归优化(栈深 O(log n))/ Introsort(深度过 2log₂n 切堆排)/ Dual-pivot(Java 基本类型)。
  • vs 归并:快排原地不稳定缓存友好;归并稳定但 O(n) 辅助。
  • vs 堆排:堆排最坏 O(n log n) 且 O(1) 空间,但常数大、缓存差,平均比快排慢 2~3 倍。
  • 交互演示快速排序可视化

一、复杂度表

情况时间复杂度触发条件递归深度
最好O(n log n)pivot 恰好是中位数,5:5 分O(log n)
平均O(n log n)随机/三数取中 pivotO(log n)
最坏O(n²)有序输入+取首元素,0:n-1 分O(n)(栈溢出风险)
空间O(log n) ~ O(n)递归栈;尾递归优化后恒 O(log n)
稳定性不稳定分区交换打乱相等元素相对序

关键数字:随机化快排期望比较次数 2n ln n ≈ 1.39 n log₂ n,仅比信息论下界 n log₂ n 多 39%。

二、Lomuto 分区模板

js
// pivot 取末元素,返回 pivot 最终下标
function lomuto(a, lo, hi) {
  const pivot = a[hi];
  let i = lo - 1;
  for (let j = lo; j < hi; j++)
    if (a[j] < pivot) { i++; [a[i], a[j]] = [a[j], a[i]]; }
  [a[i + 1], a[hi]] = [a[hi], a[i + 1]];
  return i + 1;
}
function quickSortLomuto(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const p = lomuto(a, lo, hi);
  quickSortLomuto(a, lo, p - 1);
  quickSortLomuto(a, p + 1, hi);
}

三、Hoare 分区模板

js
// pivot 取中点元素,返回分区点 j(不一定是 pivot 位)
function hoare(a, lo, hi) {
  const pivot = a[lo + (hi - lo >> 1)];
  let i = lo - 1, j = hi + 1;
  while (true) {
    do { i++; } while (a[i] < pivot);
    do { j--; } while (a[j] > pivot);
    if (i >= j) return j;
    [a[i], a[j]] = [a[j], a[i]];
  }
}
function quickSortHoare(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const p = hoare(a, lo, hi);
  quickSortHoare(a, lo, p);          // 注意:包含 p,不是 p-1
  quickSortHoare(a, p + 1, hi);
}

四、三路分区模板(重复元素最优)

js
function quickSort3Way(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return;
  const pivot = a[lo + (hi - lo >> 1)];
  let lt = lo, i = lo, gt = hi;
  while (i <= gt) {
    if (a[i] < pivot)      { [a[lt], a[i]] = [a[i], a[lt]]; lt++; i++; }
    else if (a[i] > pivot) { [a[i], a[gt]] = [a[gt], a[i]]; gt--; }
    else                   { i++; }
  }
  quickSort3Way(a, lo, lt - 1);
  quickSort3Way(a, gt + 1, hi);
}

五、工程优化模板(小数组 + 尾递归 + 三数取中)

js
const INSERT = 16;
function insertionSort(a, lo, hi) {
  for (let i = lo + 1; i <= hi; i++) {
    const key = a[i]; let j = i - 1;
    while (j >= lo && a[j] > key) { a[j + 1] = a[j]; j--; }
    a[j + 1] = key;
  }
}
function median3(a, lo, hi) {                 // 三数取中,中位数放 a[lo]
  const mid = lo + (hi - lo >> 1);
  if (a[mid] < a[lo]) [a[lo], a[mid]] = [a[mid], a[lo]];
  if (a[hi]  < a[lo]) [a[lo], a[hi] ] = [a[hi],  a[lo]];
  if (a[hi]  < a[mid])[a[mid],a[hi]] = [a[hi], a[mid]];
  return mid;
}
function quickSortEng(a, lo = 0, hi = a.length - 1) {
  while (hi - lo > INSERT) {
    median3(a, lo, hi);
    const p = lomuto(a, lo, hi);              // 配合三数取中后用
    if (p - lo < hi - p) { quickSortEng(a, lo, p - 1); lo = p + 1; }
    else                 { quickSortEng(a, p + 1, hi); hi = p - 1; }
  }
  insertionSort(a, lo, hi);
}

六、优化清单

优化作用代价谁在用
小数组切插入减少递归开销,提速 10~30%阈值调参几乎所有工业排序
尾递归优化栈深 O(n)→O(log n),防溢出代码稍复杂libstdc++/Go
三数取中规避有序退化,零额外开销3 次比较libstdc++/Java
随机化 pivot期望意义规避最坏rand() 开销教学标准实现
三路分区重复元素 O(n²)→O(n)多维护指针Java 基本类型排序
Dual-pivot随机数据再快 ~10%代码复杂Java Arrays.sort(int[])
Introsort(切堆排)保证最坏 O(n log n)检测递归深度C++ std::sort、Rust

七、与归并/堆排对比

维度快排归并排序堆排序
平均时间O(n log n)O(n log n)O(n log n)
最坏时间O(n²)(需优化规避)O(n log n)O(n log n)
额外空间O(log n)O(n) 辅助数组O(1)
稳定性❌ 不稳定✅ 稳定❌ 不稳定
缓存友好✅ 顺序扫描✅ 顺序扫描❌ 跨层跳跃
实际速度最快(常数小)次之最慢(常数大)
典型实现Introsort/C++ std::sortTimsort/JS sort优先队列场景

选型口诀:通用求快 → 快排(Introsort 兜底);要稳定 → 归并/Timsort;要 O(1) 空间+最坏保证 → 堆排。

八、易错点清单

  • 取首/末元素当 pivot + 有序输入:必退化 O(n²),新手最常踩;改用随机或三数取中。
  • Hoare 分区递归边界写错:返回的 j 不一定是 pivot 位,递归要用 (lo, j)(j+1, hi),写成 (lo, p-1)(p+1, hi) 会漏元素或死循环。
  • 三路分区 a[i] > pivoti 不自增:换回来的 a[gt] 还没看过,必须再判断一次。
  • 退化时栈溢出:百万级有序数组递归深度 O(n) 会爆栈;必须做尾递归优化。
  • 全相等输入用二路快排:退化 O(n²);必须用三路分区。
  • 阈值取太小:递归开销占比高,失去切插入排序的意义;通常 12~47。
  • 以为快排稳定:不稳定;需要稳定(如多关键字排序)要换归并/Timsort。
  • 以为 O(n²) 无法消除:Introsort 在工程上保证最坏 O(n log n)。
  • 随机化 pivot 忘记换到端点:直接用随机下标当 pivot 会让分区代码复杂,标准做法是先 swap 到端点再复用模板。

九、进阶方向(链接其他叶)

  • 归并排序:稳定的 O(n log n) 对比项 —— 见归并排序
  • 堆排序:最坏 O(n log n) 的兜底,理解 Introsort 的前提 —— 见堆排序
  • 插入排序:小数组切插入的依据 —— 见插入排序
  • 桶排序/计数排序:非比较型 O(n) 排序 —— 见线性排序
  • Top K 问题:快排分区的典型应用(快速选择 quickselect)—— 见选择算法

权威链接