排序算法里,快速排序是个绕不开的名字。我自己做了十几年后台开发,日常写业务代码时其实很少直接手撕排序,但凡是遇到需要排序的场景——榜单实时计算、TopK筛选、数据去重合并、聚合统计——脑子里第一个蹦出来的底层稳定方案,十有八九还是快排,或者披着各种外衣的快排变体。市面上各大语言的排序库,从C标准库的qsort到JDK里的Arrays.sort,底层思路要么直接是快速排序,要么就是它的改良版。这篇内容不想写成教科书式的算法讲解,而是想以实际开发者的视角,把快速排序从设计思想、核心分区逻辑、手写实现,到工程级优化和排查翻车问题的完整链路走一遍。不管你是刚接触数据结构的新手,还是写了几年代码但没细究过排序底层的老手,应该都能从中捞到点有价值的东西。
1. 快排为何封神:分治思想与速度来源解析
1.1 从“减而治之”到“分而治之”
理解快速排序前,先说说算法设计里两种极其重要的思想:减而治之和分而治之。
减而治之的典型代表是二分查找。每次把问题规模砍掉一半,但处理的对象还是同一个问题,只是范围更小了。而分而治之则是把一个大问题拆成几个独立的小问题,分别解决后再合并结果。插入排序、选择排序这类O(n^2)算法,本质上每次都在处理一个完整的大数组,效率自然上不去。归并排序是分治的经典,但它需要一个额外数组来合并结果,空间开销摆在那。
快速排序的巧妙之处在于:它既是分治思想的应用,又不像归并排序那样依赖额外空间。它的核心逻辑是选一个基准值,把数组分成两个部分,左边都比基准小,右边都比基准大,然后对左右两个部分分别递归执行同样的操作。每一次分区都确定了基准值的最终位置,也就是说,每次递归对基准值而言是一次“定死”,它不再参与后续任何移动。这个“基准值归位”的特征,让快排虽然整体上是分治,但局部上带有减而治之的味道——每轮至少有一个元素彻底排定。
1.2 平均O(n log n)的效率从哪来
很多人只背过快排平均时间复杂度是O(n log n),但没想明白这个数字是怎么来的。假设每次分区后,基准值恰好把数组切成长度基本相等的两半,那么递归深度就是log n。每一层递归里,所有分区操作加起来工作量大约是n(每一层每个元素最多被比较几次)。所以总工作量是n乘以递归深度,也就是n log n。
问题在于,这个“恰好切成两半”的假设并不总是成立。如果基准值选得很偏,比如每次都选到数组里的最大或最小值,那么分区后一边是0个元素,一边是n-1个元素,递归深度瞬间变成n,总工作量退化成O(n^2)。这就是快排最著名的“翻车点”。
这里可以做一个简单的数学对比:n=10万,O(n log n)大约要做170万次操作,而O(n^2)要做100亿次操作。差了近两个数量级。所以快排的实际速度,高度依赖“基准值选得是否够聪明”,这也正是后续所有优化工作围绕的核心命题。
1.3 内排序里的“缓存友好型”选手
除了复杂度层面的优势,快排在真实机器上还有一个容易被忽略但非常重要的优势:局部性。分区操作是原地进行的,它扫描的是数组中连续的内存区域,CPU缓存命中率极高。反观归并排序,因为需要反复把数据从原数组拷贝到临时数组,内存访问跨度更大,缓存命中率就会差一些。在千万级别以上的数据量上,这个差异甚至会直接体现在毫秒级的耗时差距上。
说实话,我当年第一次用计时器对比快排和归并时,发现快排在大数据量下经常比理论更优的、稳定的归并还要快,一度以为是归并实现得不对。后来才明白,现代CPU的缓存机制会放大局部性好的算法的优势,这属于算法理论之外的真实硬件红利。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 快排的心脏:分区算法与基准值选择实战
2.1 Lomuto分区:最好理解的教学版本
分区是快排的核心动作。最经典的分区方法之一叫Lomuto分区,思路非常直白:选最后一个元素做基准,用两个指针从左往右扫,一个指针i维护“小于基准值的区域边界”,另一个指针j负责遍历。
c复制int lomutoPartition(int arr[], int low, int high) {
int pivot = arr[high]; // 选最后一个元素为基准
int i = low - 1;
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
// 把基准放到正确位置
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pivotIndex = lomutoPartition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
这段代码逻辑很清晰,适合教学和理解快排核心思想。但它的缺点也很明显:即使数组已经完全有序,它仍然会老老实实做一轮又一轮的交换和比较,性能没有任何特殊表现。而且每次分区都要做
