当前位置: 首页 > news >正文

算法导论第七章:快速排序的艺术与科学

算法导论第七章:快速排序的艺术与科学

本文是《算法导论》精讲专栏第七章,通过分治策略可视化性能对比实验工程优化技巧,结合完整C语言实现,深入解析快速排序的精髓。包含基本实现、随机化优化、三向切分、尾递归消除等高级技术,提供10个以上完整代码实现。

一、快速排序:分治策略的巅峰之作

1.1 快速排序核心思想

graph TDA[待排序数组] --> B[选择主元]B --> C[划分:小于主元 | 主元 | 大于主元]C --> D[递归排序左半部分]C --> E[递归排序右半部分]D --> F[有序数组]E --> F

快速排序三大步骤

  1. 选择主元(Pivot):选取一个元素作为基准
  2. 划分(Partition):将数组分为小于主元和大于主元的两部分
  3. 递归排序:对左右子数组递归应用相同操作

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个元素的期望时间:

  1. 每次划分的比较次数为 n − 1 n-1 n1
  2. 划分后两部分大小分别为 i i i n − i − 1 n-i-1 ni1
  3. 递归方程为:

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)=(n1)+n1i=0n1[T(i)+T(ni1)]

推导过程
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)=(n1)+n2i=0n1T(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(n1)+2i=0n1T(i)

替换 n n n n − 1 n-1 n1
( 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) (n1)T(n1)=(n1)(n2)+2i=0n2T(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)(n1)T(n1)=2(n1)+2T(n1)

整理得:
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(n1)(nn+1)+n2(n1)

最终可得:
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,0005.215.4520
100,00071.2072.5040
1,000,000850.25865.3060
10,000,0009800.459850.2080

七、快速排序的变种与应用

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 快速排序在系统库中的应用

  1. C语言qsort函数:使用混合策略和三点中值法
  2. C++ std::sort:Introsort(快速排序+堆排序)
  3. Java Arrays.sort:对基本类型使用双枢轴快速排序
  4. 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 快速排序最佳实践

  1. 主元选择:使用三点中值法
  2. 小数组优化:当n<16时切换到插入排序
  3. 尾递归消除:减少栈空间使用
  4. 处理重复元素:使用三向切分
  5. 避免最坏情况:随机化主元选择
  6. 稳定性考虑:需要稳定排序时选择归并排序

8.2 算法选择指南

场景推荐算法理由
通用排序随机化快速排序平均性能最好
内存受限环境堆排序O(1)空间复杂度
需要稳定排序归并排序稳定且O(n log n)
大量重复元素三向切分快速排序高效处理重复元素
外部排序归并排序适合大文件处理
链表排序归并排序适合链表结构
数据基本有序插入排序O(n)时间复杂度

8.3 关键知识点总结

  1. 分治策略:快速排序是分治算法的经典应用
  2. 随机化分析:随机化避免最坏情况
  3. 原地排序:快速排序是高效的原地排序算法
  4. 工程优化:混合策略在实际应用中至关重要
  5. 变种算法:快速选择、双枢轴排序等扩展应用

“快速排序之所以快,是因为它的内循环可以在大多数架构上高效实现。在实践中,快速排序通常比其他O(n log n)算法更快,因为它的常数因子更小。”
——《算法导论》作者 Thomas H. Cormen

下章预告:第八章《线性时间排序》将探讨:

  • 计数排序的原理与实现
  • 基数排序的应用与优化
  • 桶排序的数学分析
  • 排序算法下界证明

本文完整代码已上传至GitHub仓库:Algorithm-Implementations

思考题

  1. 为什么在平均情况下快速排序比堆排序快?
  2. 如何修改快速排序算法使其变为稳定排序?
  3. 三向切分快速排序在处理重复元素时为何更高效?
  4. 在并行计算环境下,如何设计并行的快速排序算法?

相关文章:

  • 【git】如何在team里使用公共账号进行ssh clone
  • 《Vuejs设计与实现》第 9 章(简单 diff 算法)
  • java中常见的排序算法设计介绍
  • 键盘效率提升实战,快速训练指法与速度
  • SPOJ-TEST
  • NodeJS的fs模块的readFile和createReadStream区别以及常见方法
  • 区间交集:区间选点
  • LangGraph--Agent常见的模式2(并行、数据路由)
  • PaddleOCR项目实战(3):SpringBoot服务开发之全局异常处理
  • Appium+python自动化(二十四) - 元素等待
  • Spark简介脑图
  • 浏览器的事件循环和NodeJS的事件循环有什么区别
  • 【Erdas实验教程】023:遥感图像辐射增强(直方图匹配)
  • LangChain框架:AI应用开发利器
  • 最新推荐Python学习图书20本(引进版)
  • AUTOSAR Adaptive系统如何进行时间同步与延迟分析?
  • 软件设计模式入门
  • 一款完美适配mobile、pad、web三端的博客网站UI解决方案
  • 记录一次 Oracle DG 异常停库问题解决过程
  • Spring MVC 会话管理实践教程:HttpSession 深入应用
  • 做网站用什么语言最好/百度广告搜索推广
  • 网站的c4d动画是怎么做的/谷歌商店paypal官网下载
  • 自己做网站好不好/seo站内优化和站外优化
  • 什么网站做简历免费下载/上海今天刚刚发生的新闻
  • 可以在自己家做外卖的网站/百度推广服务费3000元
  • 网站建设公司起名/阿里云域名查询和注册