算法导论第七章:快速排序的艺术与科学
算法导论第七章:快速排序的艺术与科学
本文是《算法导论》精讲专栏第七章,通过分治策略可视化、性能对比实验和工程优化技巧,结合完整C语言实现,深入解析快速排序的精髓。包含基本实现、随机化优化、三向切分、尾递归消除等高级技术,提供10个以上完整代码实现。
一、快速排序:分治策略的巅峰之作
1.1 快速排序核心思想
graph TDA[待排序数组] --> 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] > pivotthree_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 <= pivot2if (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
思考题:
- 为什么在平均情况下快速排序比堆排序快?
- 如何修改快速排序算法使其变为稳定排序?
- 三向切分快速排序在处理重复元素时为何更高效?
- 在并行计算环境下,如何设计并行的快速排序算法?