快速排序在标准库里几乎是标配,但自己写的时候,不少细节都会影响性能。这是我整理快排实现时的一些笔记,从最基本的版本开始,逐步加上三数取中、小数组切换策略,最后用显式栈替代递归,避免爆栈。
最简单的快排
算法思想是分治:选一个 pivot,把数组分成左右两部分,左边都比 pivot 小,右边都比它大。pivot 的位置就确定了,然后递归处理左右。但怎么分区呢?常见的做法是 Hoare 的左右指针法。
void QuickSort1(int* a, int left, int right) {
if (left >= right) {
return;
}
int key = left; // 确定 key 的值,为第一个元素
int L = left;
int R = right; // 记录原始左右下标
while (left < right) {
// 右小人向左走,直到找到比 key 小的值
while (left < right && a[right] >= a[key]) {
right--;
}
// 左小人向右走,直到找到比 key 大的值
while (left < right && a[left] <= a[key]) {
left++;
}
Swap(&a[left], &a[right]); // 交换大的值和小的值
}
Swap(&a[right], &a[key]); // 最后交换 key 和相遇点对应的值
key = right; // key 的下标也要改变
QuickSort1(a, L, key - 1); // 递归 key 的左区间
QuickSort1(a, key + 1, R); // 递归 key 的右区间
}
这个版本固定选第一个元素为 pivot。如果数组已经有序,那每次分区就极其不平衡,一边为空,另一边是 n-1 个元素,递归深度变成 O(n),时间复杂度退化到 O(n²)。所以需要改进 pivot 的选择。
三数取中:避开最坏情况
一个简单且有效的优化是'三数取中':从第一个、中间、最后一个元素里挑一个值在中间的,换到第一位当 pivot。这样能大幅降低在常见有序或接近有序数组上退化的概率。
int FindKey(int* a, int left, int right) {
int mid = (left + right) / 2;
if (a[left] > a[right]) {
(a[right] > a[mid]) {
right;
} (a[mid] > a[left]) {
left;
} {
mid;
}
} {
(a[left] > a[mid]) {
left;
} (a[mid] > a[right]) {
right;
} {
mid;
}
}
}


