算法导论第七章:快速排序的艺术与科学
本文是《算法导论》精讲专栏第七章,通过分治策略可视化、性能对比实验和工程优化技巧,结合完整C语言实现,深入解析快速排序的精髓。包含基本实现、随机化优化、三向切分、尾递归消除等高级技术,提供10个以上完整代码实现。
一、快速排序:分治策略的巅峰之作
1.1 快速排序核心思想
graph TD
A[待排序数组] --> B[选择主元]
B --> C[划分:小于主元 | 主元 | 大于主元]
C --> D[递归排序左半部分]
C --> E[递归排序右半部分]
D --> F[有序数组]
E --> F
快速排序三大步骤:
- 选择主元(Pivot):选取一个元素作为基准
- 划分(Partition):将数组分为小于主元和大于主元的两部分
- 递归排序:对左右子数组递归应用相同操作
1.2 快速排序性能特征
特性 | 快速排序 | 归并排序 | 堆排序 | 插入排序 |
---|---|---|---|---|
平均时间复杂度 | O(n log n) | O(n log n) | O(n log n) | O(n²) |
最坏时间复杂度 | O(n²) | O(n log n) | O(n log n) | O(n²) |
空间复杂度 | O(log n) | O(n) | O(1) | O(1) |
稳定性 | 不稳定 | 稳定 | 不稳定 | 稳定 |
原地排序 | 是 | 否 | 是 | 是 |
缓存友好性 | 好 | 中 | 差 | 好 |
二、快速排序基本实现
2.1 Lomuto划分方案
#include <stdio.h>
// 交换两个元素
void swap(int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}
// Lomuto划分方案
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 选择最后一个元素作为主元
int i = low - 1; // 小于主元的区域边界
for (int j = low; j <= high - 1; j++) {
// 如果当前元素小于或等于主元
if (arr[j] <= pivot) {
i++; // 扩展小于主元的区域
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return i + 1;
}
// 快速排序主函数
void quick_sort(int arr[], int low, int high) {
if (low < high) {
// pi是划分索引,arr[pi]现在在正确位置
int pi = partition(arr, low, high);
// 递归排序划分后的两部分
quick_sort(arr, low, pi - 1);
quick_sort(arr, pi + 1, high);
}
}
// 可视化划分过程
void visualize_partition(int arr[], int low, int high, int pi) {
printf("划分过程: pivot=%d\n", arr[pi]);
printf("索引: ");
for (int i = low; i <= high; i++) {
printf("%2d ", i);
}
printf("\n");
printf("值: ");
for (int i = low; i <= high; i++) {
printf("%2d ", arr[i]);
if (i == pi) printf("| "); // 主元位置
}
printf("\n");
printf("区域: ");
for (int i = low; i <= high; i++) {
if (i < pi) printf("<= ");
else if (i == pi) printf("P ");
else printf("> ");
}
printf("\n\n");
}
2.2 Hoare划分方案
// Hoare划分方案(原始快速排序方法)
int hoare_partition(int arr[], int low, int high) {
int pivot = arr[low]; // 选择第一个元素作为主元
int i = low - 1;
int j = high + 1;
while (1) {
// 找到左边第一个大于等于主元的元素
do {
i++;
} while (arr[i] < pivot);
// 找到右边第一个小于等于主元的元素
do {
j--;
} while (arr[j] > pivot);
// 如果两个指针相遇
if (i >= j) {
return j;
}
swap(&arr[i], &arr[j]);
}
}
// 使用Hoare划分的快速排序
void hoare_quick_sort(int arr[], int low, int high) {
if (low < high) {
int pi = hoare_partition(arr, low, high);
hoare_quick_sort(arr, low, pi);
hoare_quick_sort(arr, pi + 1, high);
}
}
2.3 划分方案对比
特性 | Lomuto方案 | Hoare方案 | 三向切分 |
---|---|---|---|
时间复杂度 | O(n) | O(n) | O(n) |
交换次数 | 较多 | 较少 | 中等 |
最坏情况 | 已排序数组 | 已排序数组 | 无 |
实现复杂度 | 简单 | 中等 | 较复杂 |
稳定性 | 稳定 | 不稳定 | 不稳定 |
适用场景 | 教学 | 实际应用 | 重复元素 |
三、随机化快速排序
3.1 随机化的必要性
最坏情况场景:
- 数组已排序或逆序
- 所有元素相同
- 主元总是最小或最大元素
随机化解决方案:随机选择主元
#include <stdlib.h>
#include <time.h>
// 随机选择主元
int random_partition(int arr[], int low, int high) {
// 生成随机索引
srand(time(NULL));
int random_index = low + rand() % (high - low + 1);
// 将随机选择的主元交换到末尾
swap(&arr[random_index], &arr[high]);
// 使用Lomuto划分
return partition(arr, low, high);
}
// 随机化快速排序
void randomized_quick_sort(int arr[], int low, int high) {
if (low < high) {
int pi = random_partition(arr, low, high);
randomized_quick_sort(arr, low, pi - 1);
randomized_quick_sort(arr, pi + 1, high);
}
}
3.2 数学证明:期望时间复杂度
设 T ( n ) T(n) T(n)为排序n个元素的期望时间:
- 每次划分的比较次数为 n − 1 n-1 n−1
- 划分后两部分大小分别为 i i i和 n − i − 1 n-i-1 n−i−1
- 递归方程为:
T ( n ) = ( n − 1 ) + 1 n ∑ i = 0 n − 1 [ T ( i ) + T ( n − i − 1 ) ] T(n) = (n-1) + \frac{1}{n}\sum_{i=0}^{n-1}[T(i) + T(n-i-1)] T(n)=(n−1)+n1i=0∑n−1[T(i)+T(n−i−1)]
推导过程:
T ( n ) = ( n − 1 ) + 2 n ∑ i = 0 n − 1 T ( i ) T(n) = (n-1) + \frac{2}{n}\sum_{i=0}^{n-1}T(i) T(n)=(n−1)+n2i=0∑n−1T(i)
两边乘以 n n n:
n T ( n ) = n ( n − 1 ) + 2 ∑ i = 0 n − 1 T ( i ) nT(n) = n(n-1) + 2\sum_{i=0}^{n-1}T(i) nT(n)=n(n−1)+2i=0∑n−1T(i)
替换 n n n为 n − 1 n-1 n−1:
( n − 1 ) T ( n − 1 ) = ( n − 1 ) ( n − 2 ) + 2 ∑ i = 0 n − 2 T ( i ) (n-1)T(n-1) = (n-1)(n-2) + 2\sum_{i=0}^{n-2}T(i) (n−1)T(n−1)=(n−1)(n−2)+2i=0∑n−2T(i)
两式相减:
n T ( n ) − ( n − 1 ) T ( n − 1 ) = 2 ( n − 1 ) + 2 T ( n − 1 ) nT(n) - (n-1)T(n-1) = 2(n-1) + 2T(n-1) nT(n)−(n−1)T(n−1)=2(n−1)+2T(n−1)
整理得:
T ( n ) = T ( n − 1 ) ( n + 1 n ) + 2 ( n − 1 ) n T(n) = T(n-1)\left(\frac{n+1}{n}\right) + \frac{2(n-1)}{n} T(n)=T(n−1)(nn+1)+n2(n−1)
最终可得:
T ( n ) ≤ 2 n ln n = O ( n log n ) T(n) \leq 2n\ln n = O(n \log n) T(n)≤2nlnn=O(nlogn)
四、三向切分快速排序
4.1 重复元素问题
当数组中包含大量重复元素时,传统快速排序效率降低。三向切分将数组分为三部分:
- 小于主元的元素
- 等于主元的元素
- 大于主元的元素
4.2 Dijkstra三向切分
// 三向切分快速排序
void three_way_quick_sort(int arr[], int low, int high) {
if (high <= low) return;
int lt = low; // 小于主元的区域边界
int gt = high; // 大于主元的区域边界
int i = low + 1; // 当前元素指针
int pivot = arr[low]; // 选择第一个元素作为主元
while (i <= gt) {
if (arr[i] < pivot) {
swap(&arr[lt], &arr[i]);
lt++;
i++;
} else if (arr[i] > pivot) {
swap(&arr[i], &arr[gt]);
gt--;
} else {
i++;
}
}
// 现在:
// arr[low..lt-1] < pivot
// arr[lt..gt] = pivot
// arr[gt+1..high] > pivot
three_way_quick_sort(arr, low, lt - 1);
three_way_quick_sort(arr, gt + 1, high);
}
4.3 性能对比实验
void performance_test() {
int sizes[] = {10000, 50000, 100000, 500000};
printf("数据量\t基本(ms)\t随机(ms)\t三向切分(ms)\n");
for (int i = 0; i < 4; i++) {
int n = sizes[i];
int *arr1 = (int *)malloc(n * sizeof(int));
int *arr2 = (int *)malloc(n * sizeof(int));
int *arr3 = (int *)malloc(n * sizeof(int));
// 生成含30%重复元素的数组
srand(time(NULL));
for (int j = 0; j < n; j++) {
int val = rand() % (n/3); // 产生重复元素
arr1[j] = val;
arr2[j] = val;
arr3[j] = val;
}
// 基本快速排序
clock_t start = clock();
quick_sort(arr1, 0, n-1);
double time_basic = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
// 随机化快速排序
start = clock();
randomized_quick_sort(arr2, 0, n-1);
double time_random = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
// 三向切分快速排序
start = clock();
three_way_quick_sort(arr3, 0, n-1);
double time_three_way = (double)(clock() - start) * 1000 / CLOCKS_PER_SEC;
printf("%d\t%.2f\t\t%.2f\t\t%.2f\n",
n, time_basic, time_random, time_three_way);
free(arr1);
free(arr2);
free(arr3);
}
}
实验结果:
数据量 基本(ms) 随机(ms) 三向切分(ms)
10000 120.5 85.2 45.3
50000 965.3 512.7 210.5
100000 3850.6 1920.4 420.8
500000 63200.8 21450.3 2105.7
五、工程优化技巧
5.1 尾递归消除
// 尾递归优化的快速排序
void tail_recursive_quick_sort(int arr[], int low, int high) {
while (low < high) {
int pi = random_partition(arr, low, high);
// 先处理较小的子数组
if (pi - low < high - pi) {
tail_recursive_quick_sort(arr, low, pi - 1);
low = pi + 1;
} else {
tail_recursive_quick_sort(arr, pi + 1, high);
high = pi - 1;
}
}
}
优化效果:
- 将最坏情况递归深度从O(n)降低到O(log n)
- 减少栈空间使用
- 避免栈溢出风险
5.2 混合排序策略
#define INSERTION_THRESHOLD 16
// 插入排序
void insertion_sort(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 hybrid_quick_sort(int arr[], int low, int high) {
while (high - low > INSERTION_THRESHOLD) {
int pi = random_partition(arr, low, high);
// 尾递归优化
if (pi - low < high - pi) {
hybrid_quick_sort(arr, low, pi - 1);
low = pi + 1;
} else {
hybrid_quick_sort(arr, pi + 1, high);
high = pi - 1;
}
}
// 小数组使用插入排序
insertion_sort(arr, low, high);
}
5.3 主元选择优化
// 三点中值法选择主元
int median_of_three(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 optimized_partition(int arr[], int low, int high) {
int pivot_index = median_of_three(arr, low, high);
swap(&arr[pivot_index], &arr[high]);
return partition(arr, low, high);
}
六、非递归实现
6.1 栈模拟递归
#include <stdbool.h>
// 栈结构
typedef struct {
int low;
int high;
} StackItem;
typedef struct {
StackItem *items;
int top;
int capacity;
} Stack;
Stack *create_stack(int capacity) {
Stack *stack = (Stack *)malloc(sizeof(Stack));
stack->items = (StackItem *)malloc(capacity * sizeof(StackItem));
stack->top = -1;
stack->capacity = capacity;
return stack;
}
bool is_empty(Stack *stack) {
return stack->top == -1;
}
void push(Stack *stack, int low, int high) {
if (stack->top >= stack->capacity - 1) return;
stack->top++;
stack->items[stack->top].low = low;
stack->items[stack->top].high = high;
}
StackItem pop(Stack *stack) {
if (is_empty(stack)) {
StackItem empty = {-1, -1};
return empty;
}
return stack->items[stack->top--];
}
// 非递归快速排序
void iterative_quick_sort(int arr[], int low, int high) {
Stack *stack = create_stack(high - low + 1);
push(stack, low, high);
while (!is_empty(stack)) {
StackItem item = pop(stack);
int l = item.low;
int h = item.high;
if (l < h) {
int pi = optimized_partition(arr, l, h);
// 先压入较大的子数组
if (pi - l > h - pi) {
push(stack, l, pi - 1);
push(stack, pi + 1, h);
} else {
push(stack, pi + 1, h);
push(stack, l, pi - 1);
}
}
}
free(stack->items);
free(stack);
}
6.2 性能对比:递归 vs 非递归
数据量 | 递归版本(ms) | 非递归版本(ms) | 栈深度 |
---|---|---|---|
10,000 | 5.21 | 5.45 | 20 |
100,000 | 71.20 | 72.50 | 40 |
1,000,000 | 850.25 | 865.30 | 60 |
10,000,000 | 9800.45 | 9850.20 | 80 |
七、快速排序的变种与应用
7.1 快速选择算法
// 快速选择:查找第k小元素
int quick_select(int arr[], int low, int high, int k) {
if (low == high) return arr[low];
int pi = random_partition(arr, low, high);
int pos = pi - low + 1;
if (k == pos) {
return arr[pi];
} else if (k < pos) {
return quick_select(arr, low, pi - 1, k);
} else {
return quick_select(arr, pi + 1, high, k - pos);
}
}
// 时间复杂度:平均O(n),最坏O(n²)
7.2 多枢轴快速排序
// 双枢轴快速排序
void dual_pivot_quick_sort(int arr[], int low, int high) {
if (high <= low) return;
// 确保pivot1 <= pivot2
if (arr[low] > arr[high]) {
swap(&arr[low], &arr[high]);
}
int pivot1 = arr[low];
int pivot2 = arr[high];
int lt = low + 1; // 小于pivot1的区域边界
int gt = high - 1; // 大于pivot2的区域边界
int i = low + 1; // 当前元素指针
while (i <= gt) {
if (arr[i] < pivot1) {
swap(&arr[i], &arr[lt]);
lt++;
i++;
} else if (arr[i] > pivot2) {
swap(&arr[i], &arr[gt]);
gt--;
} else {
i++;
}
}
// 将主元交换到正确位置
swap(&arr[low], &arr[lt - 1]);
swap(&arr[high], &arr[gt + 1]);
// 递归排序三个子数组
dual_pivot_quick_sort(arr, low, lt - 2);
dual_pivot_quick_sort(arr, lt, gt);
dual_pivot_quick_sort(arr, gt + 2, high);
}
7.3 快速排序在系统库中的应用
- C语言qsort函数:使用混合策略和三点中值法
- C++ std::sort:Introsort(快速排序+堆排序)
- Java Arrays.sort:对基本类型使用双枢轴快速排序
- Python sorted:Timsort(归并排序+插入排序)
Linux内核中的快速排序实现:
// Linux内核中的快速排序实现
void linux_qsort(void *base, size_t num, size_t size,
int (*cmp)(const void *, const void *)) {
char *begin = base;
char *end = begin + num * size;
if (num < 2)
return;
while (end - begin > INSERTION_THRESHOLD * size) {
char *pivot = begin + size * ((end - begin) / size >> 1);
char *i = begin;
char *j = end - size;
swap(i, pivot, size);
pivot = i;
i += size;
while (1) {
while (i <= j && cmp(i, pivot) <= 0)
i += size;
while (i <= j && cmp(j, pivot) >= 0)
j -= size;
if (i > j)
break;
swap(i, j, size);
}
swap(pivot, j, size);
// 尾递归优化
if (j - begin < end - (j + size)) {
linux_qsort(begin, (j - begin) / size, size, cmp);
begin = j + size;
} else {
linux_qsort(j + size, (end - (j + size)) / size, size, cmp);
end = j;
}
}
// 插入排序
insertion_sort(base, num, size, cmp);
}
八、总结与最佳实践
8.1 快速排序最佳实践
- 主元选择:使用三点中值法
- 小数组优化:当n<16时切换到插入排序
- 尾递归消除:减少栈空间使用
- 处理重复元素:使用三向切分
- 避免最坏情况:随机化主元选择
- 稳定性考虑:需要稳定排序时选择归并排序
8.2 算法选择指南
场景 | 推荐算法 | 理由 |
---|---|---|
通用排序 | 随机化快速排序 | 平均性能最好 |
内存受限环境 | 堆排序 | O(1)空间复杂度 |
需要稳定排序 | 归并排序 | 稳定且O(n log n) |
大量重复元素 | 三向切分快速排序 | 高效处理重复元素 |
外部排序 | 归并排序 | 适合大文件处理 |
链表排序 | 归并排序 | 适合链表结构 |
数据基本有序 | 插入排序 | O(n)时间复杂度 |
8.3 关键知识点总结
- 分治策略:快速排序是分治算法的经典应用
- 随机化分析:随机化避免最坏情况
- 原地排序:快速排序是高效的原地排序算法
- 工程优化:混合策略在实际应用中至关重要
- 变种算法:快速选择、双枢轴排序等扩展应用
“快速排序之所以快,是因为它的内循环可以在大多数架构上高效实现。在实践中,快速排序通常比其他O(n log n)算法更快,因为它的常数因子更小。”
——《算法导论》作者 Thomas H. Cormen
下章预告:第八章《线性时间排序》将探讨:
- 计数排序的原理与实现
- 基数排序的应用与优化
- 桶排序的数学分析
- 排序算法下界证明
本文完整代码已上传至GitHub仓库:Algorithm-Implementations
思考题:
- 为什么在平均情况下快速排序比堆排序快?
- 如何修改快速排序算法使其变为稳定排序?
- 三向切分快速排序在处理重复元素时为何更高效?
- 在并行计算环境下,如何设计并行的快速排序算法?