目录
快速排序是 C.A.R Hoare 于 1960 年提出的经典排序算法,核心优势是原地排序且平均时间复杂度低(O(nlogn))。但传统快速排序在处理大量重复元素的数组时,划分效率会显著下降(最坏时间复杂度退化为O(n2))。三路划分快速排序(3-Way QuickSort)正是为解决这一问题而生,它将数组划分为 “小于基准、等于基准、大于基准” 三部分,避免对重复元素的重复处理,大幅提升含重复元素数组的排序效率。
一、传统快速排序回顾
(一)核心思想
传统快速排序基于分治法,核心步骤:
- 选基准:从数组中选择一个元素作为基准(pivot);
- 划分:将数组分为两部分,左部分≤基准,右部分≥基准,基准归位;
- 递归:分别对左右两部分递归执行快速排序。
(二)完整代码实现(带详细注释)
#include <stdio.h>
// 交换两个整数(地址传递,实现值交换)
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
/**
* 划分函数(霍尔划分法)
* @param arr 待排序数组
* @param low 左边界索引
* @param high 右边界索引
* @return 基准元素的最终位置
*/
int partition(int arr[], int low, int high) {
// 选择最右侧元素作为基准(简单但易退化,优化方案:三数取中法)
int pivot = arr[high];
// i:小于基准区域的右边界(初始为左边界左侧)
int i = (low - 1);
// 遍历[low, high-1]区间,将小于基准的元素移到左侧
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++; // 扩大小于基准区域
swap(&arr[i], &arr[j]); // 交换到小于基准区域
}
}
// 基准元素归位(i+1是基准的最终位置)
swap(&arr[i + 1], &arr[high]);
return (i + 1);
}
/**
* 传统快速排序函数
* @param arr 待排序数组
* @param low 左边界索引
* @param high 右边界索引
*/
void quickSort(int arr[], int low, int high) {
// 递归终止条件:区间长度≤1
if (low < high) {
// 划分:获取基准位置
int pi = partition(arr, low, high);
// 递归排序左半部分(小于基准)
quickSort(arr, low, pi - 1);
// 递归排序右半部分(大于基准)
quickSort(arr, pi + 1, high);
}
}
// 打印数组(辅助函数,便于验证结果)
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
// 测试示例
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
printf("初始数组: ");
printArray(arr, n);
quickSort(arr, 0, n - 1); // 排序:左边界0,右边界n-1
printf("排序后的数组: ");
printArray(arr, n);
return 0;
}
(三)传统快速排序的痛点
- 重复元素导致效率下降:当数组中存在大量重复元素时,划分结果会极度不平衡(如全部元素相同,每次划分仅能确定一个元素位置);
- 递归深度增加:不平衡的划分会导致递归深度从logn变为n,甚至触发栈溢出;
- 时间复杂度退化:最坏情况下时间复杂度从O(nlogn)退化为O(n2)。
二、三路划分快速排序原理
(一)核心思想
三路划分快速排序(也称为 “荷兰国旗问题解法”)将数组划分为三个区间:
- [low,lt−1]:小于基准(pivot)的元素;
- [lt,gt]:等于基准的元素;
- [gt+1,high]:大于基准的元素。
通过一次遍历完成三路划分,递归时只需处理 “小于基准” 和 “大于基准” 的区间,等于基准的区间无需递归,大幅减少重复元素的处理次数。
(二)荷兰国旗问题类比
荷兰国旗由红、白、蓝三色组成,要求将随机排列的三色球按 “红→白→蓝” 顺序排列,对应三路划分:
- 红色球 → 小于基准的元素;
- 白色球 → 等于基准的元素;
- 蓝色球 → 大于基准的元素。
(三)详细执行步骤
- 初始化指针:
lt(less than):小于基准区域的右边界,初始 =low;gt(greater than):大于基准区域的左边界,初始 =high;i:遍历指针,初始 =low。
- 遍历数组(i≤gt):
- 若
arr[i] < pivot:交换arr[i]和arr[lt],lt++,i++; - 若
arr[i] == pivot:i++(直接归入等于区域); - 若
arr[i] > pivot:交换arr[i]和arr[gt],gt--(i不移动,交换后的元素需重新判断)。
- 若
- 递归排序:仅递归排序
[low, lt-1](小于基准)和[gt+1, high](大于基准)区间。
(四)可视化执行过程
以数组[5, 1, 7, 5, 8, 5, 9, 5]为例,选择基准5:
| 步骤 | 数组状态 | lt | i | gt | 操作说明 |
|---|---|---|---|---|---|
| 初始 | [5, 1, 7, 5, 8, 5, 9, 5] | 0 | 0 | 7 | 基准 = 5 |
| 1 | [5, 1, 7, 5, 8, 5, 9, 5] | 0 | 1 | 7 | arr [1]=1 < 5,交换 arr [0] 和 arr [1],lt=1,i=2 |
| 2 | [1, 5, 7, 5, 8, 5, 9, 5] | 1 | 2 | 7 | arr [2]=7 > 5,交换 arr [2] 和 arr [7],gt=6,i=2 |
| 3 | [1, 5, 5, 5, 8, 5, 9, 7] | 1 | 2 | 6 | arr[2]=5 == 5,i=3 |
| 4 | [1, 5, 5, 5, 8, 5, 9, 7] | 1 | 3 | 6 | arr[3]=5 == 5,i=4 |
| 5 | [1, 5, 5, 5, 8, 5, 9, 7] | 1 | 4 | 6 | arr [4]=8 > 5,交换 arr [4] 和 arr [6],gt=5,i=4 |
| 6 | [1, 5, 5, 5, 9, 5, 8, 7] | 1 | 4 | 5 | arr [4]=9 > 5,交换 arr [4] 和 arr [5],gt=4,i=4 |
| 7 | [1, 5, 5, 5, 5, 9, 8, 7] | 1 | 4 | 4 | arr[4]=5 == 5,i=5 |
| 结束 | [1, 5, 5, 5, 5, 9, 8, 7] | 1 | 5 | 4 | 遍历终止(i>gt) |
最终划分结果:
- 小于基准:
[1](索引 0); - 等于基准:
[5,5,5,5](索引 1-4); - 大于基准:
[9,8,7](索引 5-7)。
三、三路划分快速排序完整实现
#include <stdio.h>
// 交换两个整数
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
/**
* 三路划分函数
* 将数组 arr[low..high] 分成三部分:
* < pivot:arr[low .. lt-1]
* == pivot:arr[lt .. gt]
* > pivot:arr[gt+1 .. high]
*/
void partition3Way(int arr[], int low, int high, int *lt, int *gt) {
// 这里选最左边作为基准,也可改用三数取中优化
int pivot = arr[low];
*lt = low;
*gt = high;
int i = 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)--;
// i 不前进,因为换过来的是新数,还要再判断
} else {
// 等于基准:直接跳过
i++;
}
}
}
/**
* 三路快速排序主函数
*/
void quickSort3Way(int arr[], int low, int high) {
if (low >= high) {
return;
}
int lt, gt;
partition3Way(arr, low, high, <, >);
// 递归:只排 小于pivot 和 大于pivot 的部分
quickSort3Way(arr, low, lt - 1);
quickSort3Way(arr, gt + 1, high);
}
// 打印数组
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
// 大量重复元素的测试用例
int arr[] = {5, 1, 7, 5, 8, 5, 9, 5, 3, 5, 2, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("排序前:");
printArray(arr, n);
quickSort3Way(arr, 0, n - 1);
printf("排序后:");
printArray(arr, n);
return 0;
}
运行结果:
排序前:5 1 7 5 8 5 9 5 3 5 2 5
排序后:1 2 3 5 5 5 5 5 5 7 8 9
四、三路快排为什么能解决重复元素问题?
传统快排:
- 只分成 小于 / 大于等于 两部分
- 大量重复时,划分极度不平衡,退化成接近 O (n²)
三路快排:
- 分成 小于 / 等于 / 大于 三部分
- 等于基准的元素一次性全部就位
- 递归只处理两边,重复元素不再参与任何递归和交换
时间复杂度:
- 最好 / 平均:O (n log n)
- 大量重复时仍保持 O (n log n),不会退化
五、三路快排与传统快排对比
| 对比项 | 传统快速排序 | 三路划分快速排序 |
|---|---|---|
| 划分区域 | 2 部分:< pivot、≥ pivot | 3 部分:<、==、> pivot |
| 重复元素处理 | 效率差,容易退化 | 极高效,重复元素一次性搞定 |
| 指针数量 | 2 个指针 | 3 个指针(lt、i、gt) |
| 适用场景 | 元素重复少的随机数据 | 含大量重复元素的数组(最擅长) |
| 实际工程使用 | 基础库常用(配合三数取中) | Java、C++ STL 中 sort 的核心优化 |
六、适用场景
三路快速排序特别适合:
- 数组中有大量重复整数 / 浮点数
- 对成绩、年龄、分数、状态码等进行排序
- 嵌入式、数据处理、算法竞赛中追求稳定高效
- 需要避免传统快排最坏情况的场景
七、总结
- 传统快排:分治法,分成两部分,遇到大量重复元素效率下降。
- 三路快排:
- 一次遍历分成:小于、等于、大于 三部分
- 等于基准的元素不再递归,大幅减少交换与比较
- 对重复元素多的数组,性能远超传统快排
- 核心思想来自 荷兰国旗问题,是工程中最实用的快排优化之一。
掌握三路划分快速排序,你就掌握了应对含大量重复数据的最强排序方案之一。
&spm=1001.2101.3001.5002&articleId=146489359&d=1&t=3&u=258873d8af3e4eb598dfe4d57eb97981)
8766

被折叠的 条评论
为什么被折叠?



