快速排序(Quicksort)作为20世纪最重要的算法之一,由Tony Hoare于1959年发明。这种基于分治策略的排序算法因其卓越的平均性能而成为实际应用中最常用的排序算法之一。其核心思想是通过递归地将数据集分解为较小的子集来实现排序。
在标准实现中,快速排序的时间复杂度在平均情况下为O(n log n),这使得它比许多其他O(n²)的排序算法(如冒泡排序或插入排序)更为高效。然而,算法性能高度依赖于枢轴(pivot)的选择策略,这也是不同实现版本的主要区别所在。
在实际应用中,我们通常面临四种主要的枢轴选择策略:
提示:对于近乎有序的输入数据,固定选择首/末元素作为枢轴会导致算法退化为O(n²)的时间复杂度,这是实际应用中需要特别注意的。
选择第一个元素作为枢轴(即本文的实现方案)具有以下特点:
优势:
劣势:
分区(partition)是快速排序的核心操作,其目标是将数组划分为两个部分:小于枢轴的元素和大于枢轴的元素。本文实现的独特之处在于采用了从右向左的扫描方式:
c复制int partition(int arr[], int low, int high) {
int pivot = arr[low]; // 选择首元素为枢轴
int k = high; // 初始化交换位置指针
for (int i = high; i > low; i--) {
if (arr[i] > pivot) {
swap(&arr[i], &arr[k--]); // 将大于枢轴的元素移到右侧
}
}
swap(&arr[low], &arr[k]); // 将枢轴放到最终位置
return k;
}
这个实现与经典的Lomuto分区方案或Hoare原始分区方案有所不同,它通过从数组末尾开始扫描,将所有大于枢轴的元素"堆积"在数组右侧。
快速排序的递归结构体现了典型的分治思想:
c复制void quickSort(int arr[], int low, int high) {
if (low < high) {
int idx = partition(arr, low, high); // 获取枢轴位置
quickSort(arr, low, idx - 1); // 排序左子数组
quickSort(arr, idx + 1, high); // 排序右子数组
}
}
每次分区操作后,算法会确定枢轴元素的最终位置,然后递归处理枢轴左右两侧的子数组。递归的基本情况是子数组长度小于2(low >= high),此时数组已经有序。
让我们通过具体示例详细分析算法的执行流程。考虑数组arr[] = {7, 6, 10, 5, 9, 2, 1, 15, 7}:
初始状态:
扫描过程:
最终交换:
第一次分区后,数组分为两个子数组:
对左子数组进行分区:
继续递归直到所有子数组有序。
在最佳情况下,每次分区都能将数组完美平分:
最坏情况发生在数组已经有序(正序或逆序)时:
对于随机排列的输入,数学期望显示:
本文实现的空间复杂度主要来自递归调用栈:
可以通过尾递归优化减少栈空间使用:
c复制void quickSortTail(int arr[], int low, int high) {
while (low < high) {
int idx = partition(arr, low, high);
if (idx - low < high - idx) {
quickSortTail(arr, low, idx - 1);
low = idx + 1;
} else {
quickSortTail(arr, idx + 1, high);
high = idx - 1;
}
}
}
这种优化确保递归调用只发生在较小的子数组上,将最坏情况栈空间降至O(log n)。
对于小规模数组(通常n<15),插入排序可能更高效:
c复制void quickSortOptimized(int arr[], int low, int high) {
if (high - low < 15) {
insertionSort(arr, low, high);
return;
}
// 正常快速排序逻辑
}
当数组包含大量重复元素时,标准快速排序效率会下降。可采用三向切分优化:
c复制void quickSort3Way(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);
}
为避免最坏情况,实际应用中常结合多种策略:
c复制#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define INSERTION_THRESHOLD 15
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void insertionSort(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;
}
}
int medianOfThree(int arr[], int low, int high) {
int mid = low + (high - low) / 2;
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;
}
int partition(int arr[], int low, int high) {
// 使用三数取中法选择枢轴
int pivotIndex = medianOfThree(arr, low, high);
swap(&arr[low], &arr[pivotIndex]);
int pivot = arr[low];
int k = high;
for (int i = high; i > low; i--) {
if (arr[i] > pivot) {
swap(&arr[i], &arr[k--]);
}
}
swap(&arr[low], &arr[k]);
return k;
}
void quickSort(int arr[], int low, int high) {
while (high - low > INSERTION_THRESHOLD) {
int idx = partition(arr, low, high);
// 尾递归优化
if (idx - low < high - idx) {
quickSort(arr, low, idx - 1);
low = idx + 1;
} else {
quickSort(arr, idx + 1, high);
high = idx - 1;
}
}
insertionSort(arr, low, high);
}
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
void testSorting() {
int testCases[][20] = {
{7, 6, 10, 5, 9, 2, 1, 15, 7},
{1, 2, 3, 4, 5},
{5, 4, 3, 2, 1},
{3, 1, 4, 1, 5, 9, 2, 6, 5},
{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
{1}
};
int sizes[] = {9, 5, 5, 9, 10, 1};
for (int i = 0; i < sizeof(sizes)/sizeof(sizes[0]); i++) {
printf("Test case %d: ", i+1);
printArray(testCases[i], sizes[i]);
quickSort(testCases[i], 0, sizes[i]-1);
printf("Sorted: ");
printArray(testCases[i], sizes[i]);
printf("\n");
}
}
int main() {
srand(time(NULL));
testSorting();
return 0;
}
在实际项目中评估排序算法性能时,应考虑以下测试场景:
| 特性 | 快速排序 | 归并排序 |
|---|---|---|
| 时间复杂度 | 平均O(n log n) | 稳定O(n log n) |
| 空间复杂度 | O(log n) | O(n) |
| 稳定性 | 不稳定 | 稳定 |
| 缓存局部性 | 优秀 | 较差 |
| 最坏情况 | O(n²) | O(n log n) |
| 特性 | 快速排序 | 堆排序 |
|---|---|---|
| 平均性能 | 更快 | 较慢 |
| 最坏情况 | O(n²) | O(n log n) |
| 空间复杂度 | O(log n) | O(1) |
| 实现复杂度 | 中等 | 较复杂 |
| 数据敏感度 | 敏感 | 不敏感 |
在实际系统排序实现中(如C的qsort、C++的std::sort),通常采用快速排序与插入排序、堆排序结合的混合策略,以兼顾各种情况下的性能表现。
快速选择(Quickselect)是基于快速排序分区思想的选择算法,可以在平均O(n)时间内找到第k小元素:
c复制int quickSelect(int arr[], int low, int high, int k) {
if (low == high) return arr[low];
int idx = partition(arr, low, high);
int pivotRank = idx - low + 1;
if (k == pivotRank) {
return arr[idx];
} else if (k < pivotRank) {
return quickSelect(arr, low, idx - 1, k);
} else {
return quickSelect(arr, idx + 1, high, k - pivotRank);
}
}
利用多核处理器实现并行排序:
c复制#include <omp.h>
void parallelQuickSort(int arr[], int low, int high) {
if (low < high) {
int idx = partition(arr, low, high);
#pragma omp parallel sections
{
#pragma omp section
parallelQuickSort(arr, low, idx - 1);
#pragma omp section
parallelQuickSort(arr, idx + 1, high);
}
}
}
对于无法全部装入内存的大数据集,可采用外部快速排序:
快速排序自1959年发明以来,经历了多次重要改进:
现代系统中的应用实例:
在实际工程中选择排序算法时,除了时间复杂度外,还需要考虑以下因素: