Back to Journal
02 / Entry· 2 min read

排序算法系列 07:快速排序——面试必问,工程必用

详解快速排序的分区思想、原地实现、最坏情况规避策略,以及它为什么实际跑得比归并快

🔊 系统朗读

排序算法系列 07:快速排序——面试必问,工程必用

快速排序大概是排序算法里"地位最高"的一个。面试高频考、工程广泛用、各种语言标准库里都能看到它的影子。

为什么?因为它平均性能极好,空间开销又小。虽然最坏情况可能退化到 O(n²),但经过简单优化后,实际场景中这种情况几乎不会发生。

核心思想

选一个元素当"基准",把比它小的扔左边,比它大的扔右边。这样基准就到了最终正确的位置。然后递归处理左右两部分。

text
原始:5, 2, 9, 1, 7

选 5 做基准:
  左边(比 5 小):2, 1
  基准:5
  右边(比 5 大):9, 7

递归处理左边 → 1, 2
递归处理右边 → 7, 9

最终:1, 2, 5, 7, 9

每次分区(partition)都让一个元素归位。递归下去,所有元素都归位了。

和归并排序的区别

两个都是分治,但顺序相反:

  • 归并排序:先拆后治——拆的时候不做任何比较,排序工作在合并时完成
  • 快速排序:先治后拆——分区的时候就在排序,分完之后不需要合并

所以快排不需要额外的合并数组,空间开销更小。

简单版本(好理解)

这个版本会创建额外数组,不是原地排序,但最容易看懂:

javascript
function quickSort(arr) {
  if (arr.length <= 1) return arr;

  const pivot = arr[0];
  const left = [];
  const right = [];

  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }

  return quickSort(left).concat(pivot, quickSort(right));
}

console.log(quickSort([5, 2, 9, 1, 7]));
// [1, 2, 5, 7, 9]

逻辑一目了然:选基准、分两堆、递归、拼起来。

原地分区版本(实际常用)

真正工程里用的是原地版本——不创建新数组,直接在原数组上操作:

javascript
function quickSortInPlace(arr) {
  const result = [...arr];

  function sort(left, right) {
    if (left >= right) return;

    const pivotIndex = partition(result, left, right);
    sort(left, pivotIndex - 1);
    sort(pivotIndex + 1, right);
  }

  sort(0, result.length - 1);
  return result;
}

function partition(arr, left, right) {
  const pivot = arr[right]; // 取最后一个元素做基准
  let i = left;

  for (let j = left; j < right; j++) {
    if (arr[j] < pivot) {
      [arr[i], arr[j]] = [arr[j], arr[i]];
      i++;
    }
  }

  [arr[i], arr[right]] = [arr[right], arr[i]];
  return i;
}

console.log(quickSortInPlace([5, 2, 9, 1, 7]));
// [1, 2, 5, 7, 9]

partition 做的事情:遍历一遍,把比基准小的都换到左边,最后把基准放到中间。返回基准的最终位置。

变量 i 可以理解为"小于基准的区域的右边界"。每发现一个比基准小的元素,就把它换到这个边界处,然后边界右移一位。

复杂度

情况时间复杂度
最好O(n log n)
平均O(n log n)
最坏O(n²)

最坏情况发生在每次选的基准都是当前区间的最大或最小值——这样每次分区只能排除一个元素,退化成 O(n²)。比如对已排序数组用第一个元素做基准,就会中招。

空间复杂度:主要是递归调用栈,平均 O(log n),最坏 O(n)

稳定性:不稳定。分区时元素会跨距离交换。

怎么避免最坏情况?

实际工程中都会做优化:

  1. 随机选基准:不固定取第一个或最后一个,而是随机选一个。这样极端退化的概率趋近于零
  2. 三数取中:取头、尾、中间三个数的中位数做基准,效果很好
  3. 小数组切插入排序:递归到一定深度,子数组很小时切到插入排序
  4. Introsort 策略:递归深度超过阈值时切到堆排序,彻底避免 O(n²)

C++ STL 的 std::sort 用的就是 Introsort(快排 + 堆排 + 插入排序的混合体)。

快排为什么实际跑得快?

从复杂度看,快排和归并都是 O(n log n)。但快排通常更快,原因是:

  • 原地操作:不需要额外数组,缓存友好性好
  • 分区操作顺序访问内存:CPU 缓存命中率高
  • 常数因子小:相比归并少了大量的数组创建和复制

这也是为什么大多数语言标准库的默认排序底层都以快排为骨架。

总结一下

快排的精髓:选基准、做分区、递归处理。三步走,简单粗暴但有效。

它最大的弱点是最坏情况,但加上随机化或三数取中后,实际场景中几乎不可能触发。

下一篇来看堆排序——另一个 O(n log n) 的选手,它的特点是时间复杂度永远稳定,不会退化。


系列导航

上一篇:排序算法系列 06:归并排序——分治思想的教科书级应用

下一篇:排序算法系列 08:堆排序——用数据结构的力量排序

分享
← 返回博客列表
🎁 有邀请福利哦,点击查看
🎁