快速排序(Quick Sort)是计算机科学中最著名的高效排序算法之一,由Tony Hoare于1959年发明。本文将全面介绍快速排序的原理、多种实现方式以及常见优化技巧。
算法原理
快速排序采用分治策略(Divide and Conquer),基本思想是:
- 选择基准值(Pivot):从数组中选择一个元素作为基准
- 分区(Partition):将数组重新排列,使小于基准的元素都在基准前面,大于基准的都在后面
- 递归排序:对基准前后的子数组递归地进行快速排序
基础实现
1. Lomuto分区方案
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = low; // 小于基准的边界
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
swap(arr[i], arr[j]);
i++;
}
}
swap(arr[i], arr[high]);
return i;
}
void quickSort(vector<int>& arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
2. Hoare分区方案
int partitionHoare(vector<int>& arr, int low, int high) {
int pivot = arr[(low + high) / 2];
int i = low - 1;
int j = high + 1;
while (true) {
do { i++; } while (arr[i] < pivot);
do { j--; } while (arr[j] > pivot);
if (i >= j) return j;
swap(arr[i], arr[j]);
}
}
void quickSortHoare(vector<int>& arr, int low, int high) {
if (low < high) {
int pi = partitionHoare(arr, low, high);
quickSortHoare(arr, low, pi);
quickSortHoare(arr, pi + 1, high);
}
}
分区方案对比
特性 | Lomuto分区 | Hoare分区 |
---|---|---|
实现复杂度 | 较简单 | 稍复杂 |
交换次数 | 较多 | 较少 |
基准选择 | 通常选最后一个元素 | 通常选中间元素 |
递归调用 | 基准不包含在子数组中 | 基准可能包含在子数组中 |
效率 | 一般 | 通常更快 |
优化策略
1. 随机化快速排序
int partitionRandom(vector<int>& arr, int low, int high) {
srand(time(NULL));
int random = low + rand() % (high - low);
swap(arr[random], arr[high]);
return partition(arr, low, high);
}
2. 三数取中法
int medianOfThree(vector<int>& arr, int low, int high) {
int mid = low + (high - low) / 2;
// 确保arr[low] <= arr[mid] <= arr[high]
if (arr[low] > arr[mid]) swap(arr[low], arr[mid]);
if (arr[low] > arr[high]) swap(arr[low], arr[high]);
if (arr[mid] > arr[high]) swap(arr[mid], arr[high]);
return mid;
}
3. 小数组优化
void insertionSort(vector<int>& arr, int low, int high) {
for (int i = low + 1; i <= high; i++) {
int key = arr[i];
int j = i - 1;
while (j >= low && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
void optimizedQuickSort(vector<int>& arr, int low, int high) {
if (high - low < 16) { // 阈值可根据实际情况调整
insertionSort(arr, low, high);
return;
}
int pi = partition(arr, low, high);
optimizedQuickSort(arr, low, pi - 1);
optimizedQuickSort(arr, pi + 1, high);
}
4. 三向切分(处理大量重复元素)
void quickSort3Way(vector<int>& arr, int low, int high) {
if (high <= low) return;
int lt = low, gt = high;
int pivot = arr[low];
int i = low;
while (i <= gt) {
if (arr[i] < pivot) {
swap(arr[lt++], arr[i++]);
} else if (arr[i] > pivot) {
swap(arr[i], arr[gt--]);
} else {
i++;
}
}
quickSort3Way(arr, low, lt - 1);
quickSort3Way(arr, gt + 1, high);
}
算法分析
时间复杂度
情况 | 时间复杂度 |
---|---|
最优情况 | O(n log n) |
平均情况 | O(n log n) |
最坏情况 | O(n²) |
最坏情况发生在每次分区都极不平衡时(如已排序数组)
空间复杂度
情况 | 空间复杂度 |
---|---|
最优/平均 | O(log n) |
最坏情况 | O(n) |
空间消耗主要来自递归调用栈
稳定性
快速排序是不稳定的排序算法,因为分区过程中相等元素的相对位置可能改变。
实际应用
- C++标准库:
std::sort()
通常使用快速排序的优化版本(如Introsort) - 大数据排序:快速排序常被用于大规模数据排序
- 嵌入式系统:内存受限环境下,快速排序比归并排序更受欢迎
常见问题
Q1: 为什么快速排序比归并排序快?
虽然两者都是O(n log n)算法,但快速排序的常数因子更小,且是原地排序(不需要额外空间)。
Q2: 如何避免最坏情况?
- 使用随机化选择基准
- 采用三数取中法
- 当递归深度过大时切换到堆排序(Introsort策略)
Q3: 什么时候不适合用快速排序?
- 需要稳定排序时
- 内存非常受限时(递归可能导致栈溢出)
- 数据几乎有序时(插入排序可能更好)
总结
快速排序因其优秀的平均性能成为最常用的排序算法之一。通过选择合适的基准、优化分区过程和处理特殊情况,可以显著提高其性能。理解快速排序不仅有助于编写高效的排序代码,也是学习分治算法思想的绝佳案例。