快速排序
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法。其核心思想是分治:任取待排序序列中的某元素作为基准值,将集合分割成两子序列,左子序列所有元素均小于基准值,右子序列所有元素均大于基准值,然后对左右子序列重复该过程,直到所有元素排列在相应位置上。
1. Hoare 版本
Hoare 分区方案是快速排序最经典的实现方式。简单来说,就是选定一个基准值(通常选第一个),通过两个指针从两端向中间扫描,将比基准小的放左边,比基准大的放右边。
以数组 [6, 1, 2, 7, 9, 3, 4, 5, 10, 8] 为例,假设 6 为基准。左边找大,右边找小,找到后互换。一趟下来,6 的左边都比它小,右边都比它大。随后以 6 为分界线,递归处理左右区间,这就像一棵二叉树的构建过程。
基础代码实现
void QuickSort(int* a, int left, int right) {
if (left >= right) return; // 区间不存在时直接返回
int keyi = left;
int begin = left;
int end = right;
while (begin < end) {
// 从右向左找小于基准的值
while (begin < end && a[end] >= a[keyi]) {
end--;
}
// 从左向右找大于基准的值
while (begin < end && a[begin] <= a[keyi]) {
begin++;
}
Swap(&a[begin], &a[end]);
}
// 将基准放到正确位置
Swap(&a[keyi], &a[end]);
// 递归排序子数组
QuickSort(a, left, end - 1);
QuickSort(a, end + 1, right);
}
为什么右边先走?
这是一个常见的面试考点。如果左边先走,当遇到比基准大的元素停止,而右边还没找到比基准小的元素时,可能会发生 begin == end 的情况,导致死循环或逻辑错误。右边先走能确保 end 最终停在比基准小的位置,保证 Swap 的安全性。
2. 算法优化
虽然基本快速排序很快,但在输入数组已经有序或接近有序时,性能会退化为 O(n²)。实际工程中常采用以下两种优化策略。
三数取中法
选择第一个元素作为基准在有序数组下会导致分区极度不平衡。三数取中法通过选取左端、中间和右端三个元素的中值作为基准,能有效避免最坏情况。
int GetMidi(int* a, int left, int right) {
int midi = (left + right) / 2;
if (a[left] < a[midi]) {
if (a[midi] < a[right]) {
return midi;
} else if (a[left] > a[right]) {
return left;
} else {
return right;
}
} else {
if (a[midi] > a[right]) {
return midi;
} else if (a[left] < a[right]) {
return left;
} else {
return right;
}
}
}
小区间优化
当区间长度较小时,递归调用的开销可能超过排序本身的代价。此时切换为插入排序效率更高。通常认为区间长度小于 10 时进行优化。
if ((right - left + 1) < 10) {
InsertSort(a + left, right - left + 1);
}
完整优化代码示例
#include <stdio.h>
void Swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
// 插入排序辅助函数
void InsertSort(int* a, int n) {
for (int i = 1; i < n; i++) {
int key = a[i];
int j = i - 1;
while (j >= 0 && a[j] > key) {
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
}
// 三数取中
int GetMidi(int* a, int left, int right) {
int midi = (left + right) / 2;
if (a[left] < a[midi]) {
if (a[midi] < a[right]) return midi;
else if (a[left] > a[right]) return left;
else return right;
} else {
if (a[midi] > a[right]) return midi;
else if (a[left] < a[right]) return left;
else return right;
}
}
// 优化后的快速排序
void QuickSort(int* a, int left, int right) {
if (left >= right) return;
// 小区间优化
if ((right - left + 1) < 10) {
InsertSort(a + left, right - left + 1);
return;
}
// 三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int begin = left + 1;
int end = right;
while (begin <= end) {
while (begin <= end && a[end] >= a[keyi]) end--;
while (begin <= end && a[begin] <= a[keyi]) begin++;
if (begin < end) Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[end]);
QuickSort(a, left, end - 1);
QuickSort(a, end + 1, right);
}
int main() {
int arr[] = {2, 1, 3};
QuickSort(arr, 0, 2);
for (int i = 0; i < 3; i++) {
printf("%d ", arr[i]);
}
return 0;
}
3. 前后指针法
这是另一种常见的快速排序实现,使用 prev 和 cur 两个指针遍历数组,将小于基准值的元素移动到前面。
排序过程
- 定义
prev指向left,cur指向prev + 1。 cur向右移动,若a[cur]小于基准值,则prev前进一步并交换a[prev]和a[cur]。- 最后将基准值交换到
prev位置。
int PartSort_TwoPointer(int* a, int left, int right) {
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right) {
if (a[cur] < a[keyi] && ++prev != cur) {
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort_TwoPointer(int* a, int left, int right) {
if (left >= right) return;
int keyi = PartSort_TwoPointer(a, left, right);
QuickSort_TwoPointer(a, left, keyi - 1);
QuickSort_TwoPointer(a, keyi + 1, right);
}
4. 非递归实现(栈模拟)
递归实现简洁,但大规模数据可能导致栈溢出。非递归实现通过显式栈模拟递归过程,避免系统栈开销。
实现思路
- 初始化栈,将初始左右下标入栈(注意顺序,先右后左)。
- 循环取出栈顶的下标,进行分区操作。
- 将分区后的左右子数组下标入栈,继续处理直到栈为空。
// 假设已实现栈结构 ST
void QuickSortNonR(int* a, int left, int right) {
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st)) {
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int keyi = PartSort_TwoPointer(a, begin, end);
if (keyi + 1 < end) {
STPush(&st, end);
STPush(&st, keyi + 1);
}
if (begin < keyi - 1) {
STPush(&st, keyi - 1);
STPush(&st, begin);
}
}
STDestroy(&st);
}
复杂度分析
- 时间复杂度:平均 O(n log n),最坏 O(n²)。
- 空间复杂度:最佳 O(log n)(递归栈深度),最坏 O(n)。
总结
快速排序凭借分治策略成为应用最广泛的排序算法之一。理解 Hoare 分区、前后指针法以及非递归实现的差异至关重要。在实际开发中,结合三数取中和小区间优化能有效规避极端数据带来的性能风险,确保算法在工程场景下的稳定性。

