快速排序
快速排序先用一个基准值把数组分成“小的在左、大的在右”,再递归排序左右两边。
一句话算法
快速排序先用一个基准值把数组分成“小的在左、大的在右”,再递归排序左右两边。
问题模型
给定一个长度为
快速排序要解决的核心子问题是:
如何在线性时间内,把区间
[l,r]按某个基准值pivot划分成两个更小的区间?
划分完成后:
- 左边元素都不大于基准值。
- 右边元素都不小于基准值。
然后分别递归处理左右区间。
核心直觉
快速排序像是在整理一堆卡片:
- 先拿一张卡片作为基准。
- 把比它小的卡片放左边。
- 把比它大的卡片放右边。
- 基准附近已经处在正确的相对位置。
- 左右两堆继续用同样的方法整理。
它快的原因不是每次只交换相邻元素,而是一次划分就能排除大量跨区间的逆序关系。
算法步骤
双路快速排序
- 若区间长度小于等于
1,直接返回。 - 选择基准值
pivot,通常取中点或随机位置。 - 左指针从左往右找第一个
>= pivot的元素。 - 右指针从右往左找第一个
<= pivot的元素。 - 如果两个指针还没交错,交换它们,然后继续移动。
- 指针交错后,递归处理左右两段。
三路快速排序
当数组里有大量重复元素时,普通快速排序会反复处理等于基准值的元素。三路划分把区间直接分成:
< pivot | == pivot | > pivot
中间等于基准值的一整段已经有序,不需要递归。
算法证明
划分不变量:
在双路划分过程中:
- 左指针左侧已经检查过的元素都
<= pivot。 - 右指针右侧已经检查过的元素都
>= pivot。 - 未检查的元素只在两个指针之间。
每次移动指针都会跳过已经在正确侧的元素;每次交换都会把一对放错侧的元素放回正确侧。因此划分结束后,左段元素都不大于基准值,右段元素都不小于基准值。
递归时,每个子问题都是同样的排序问题,只是区间更短。长度为 0 或 1 的区间天然有序。由递归归纳可知,所有子区间最终有序,整个数组也有序。
复杂度分析
一次划分是
- 平均时间复杂度:
。 - 最坏时间复杂度:
,例如基准值长期极端偏斜。 - 额外空间复杂度:平均
,来自递归栈;最坏 。
随机选基准或取中点可以降低被有序数据卡到最坏情况的风险。三路快速排序在重复元素很多时更稳。
代码实现
双路快速排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;
int a[maxn];
int n;
void quick_sort(int l, int r) {
// 1. 递归出口:如果区间只有一个数或没有数,直接返回
if (l >= r) return;
// 2. 选取基准数 (Pivot)
// 建议取中间的数,防止在原本有序的数组上退化成 O(N^2)
int mid = a[(l + r) / 2];
// 定义双指针
int i = l, j = r;
// 3. Partition 分区操作
// 目标:让左边 [l, j] 所有的数 <= mid
// 让右边 [i, r] 所有的数 >= mid
while (i <= j) {
// 左指针向右找,直到找到一个 >= mid 的数停下
// 注意:这里必须是 < mid,不能是 <=。遇到等于 mid 的也要停下,
// 这样可以将重复的 mid 均匀分散到两边,避免树倾斜。
while (a[i] < mid) i++;
// 右指针向左找,直到找到一个 <= mid 的数停下
while (a[j] > mid) j--;
// 如果指针没有交错,说明找到了一对“放错位置”的数,交换它们
if (i <= j) {
swap(a[i], a[j]);
i++;
j--;
}
}
// 4. 递归处理子区间
// 此时指针已经“错车”了:j 在左边,i 在右边 (j < i)
// 分割点变成了 j 和 i
// 递归处理左半段 [l ... j]
if (l < j) quick_sort(l, j);
// 递归处理右半段 [i ... r]
if (i < r) quick_sort(i, r);
}
int main() {
// 读写加速
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
quick_sort(1, n);
for (int i = 1; i <= n; ++i) cout << a[i] << " ";
cout << endl;
return 0;
}
三路快速排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
int a[maxn];
int n;
// 三路快速排序 3-Way Quick Sort
void quick_sort(int l, int r) {
if (l >= r) return;
// 1. 随机选一个基准数 (防止被针对卡成 O(N^2))
// 也可以简单写成 int key = a[(l+r)/2];
int rand_idx = l + rand() % (r - l + 1);
swap(a[l], a[rand_idx]);
int key = a[l]; // 选取 a[l] 作为 key,并以此展开
// 2. 定义指针
int lt = l; // lt (less than) : 指向 "等于区" 的第一个位置
int gt = r; // gt (greater than): 指向 "等于区" 的最后一个位置
int i = l + 1; // i : 当前扫描到的位置 (从 l+1 开始,因为 a[l] 是 key)
// 3. 扫描并分类
while (i <= gt) {
if (a[i] < key) {
// 情况A: 发现比 key 小的,扔到左边 (lt)
// 把 a[i] 和 a[lt] 交换,然后 lt 和 i 都右移
swap(a[i], a[lt]);
lt++;
i++;
}
else if (a[i] > key) {
// 情况B: 发现比 key 大的,扔到右边 (gt)
// 把 a[i] 和 a[gt] 交换,gt 左移
// 注意:i 不能动!因为从 gt 换回来的数还没检查过
swap(a[i], a[gt]);
gt--;
}
else {
// 情况C: 等于 key,直接跳过,i 右移
i++;
}
}
// 此时数组状态:
// [l ... lt-1] 都是 < key
// [lt ... gt] 都是 == key (这一段已经排好了,不需要递归!)
// [gt+1 ... r] 都是 > key
// 4. 递归处理左右两边
quick_sort(l, lt - 1);
quick_sort(gt + 1, r);
}
int main() {
// 基础输入输出优化
ios::sync_with_stdio(false); cin.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> a[i];
// 种子随机数,防止被黑客数据卡死
srand(time(0));
quick_sort(1, n);
for(int i = 1; i <= n; ++i) cout << a[i] << (i == n ? "" : " ");
cout << "\n";
return 0;
}
测试用例
输入:
8
5 1 9 2 5 3 8 4
输出:
1 2 3 4 5 5 8 9
应用分类详解
快速排序的本质是“按基准值划分候选空间”,它不只是一种排序算法,也是一类分治建模方式。
一、通用排序
典型模式: 需要把数组整体排序,允许原地修改数组。 识别信号: 只需要最终有序,不要求稳定排序。 核心建模: 每轮划分让跨左右区间的大小关系正确,再递归处理内部。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 排序模板 | 排序基础题 | 双路快速排序或直接使用 std::sort |
| 大量重复元素排序 | 本文三路模板 | 等于基准的一段不再递归 |
二、快速选择
典型模式: 只需要第 k 个位置的一侧。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 第 k 小 | luogu-P1923 | 快速选择只保留一边递归 |
| Top K | LeetCode 215 | 按排名决定递归方向 |
三、分治思想训练
典型模式: 先把问题按某个标准拆成两边,再分别处理。 识别信号: 问题有明显的“划分后互不干扰”的结构。 核心建模: 找到一个划分操作,使左右子问题规模变小且性质保持一致。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 归并排序 | 排序基础题 | 先递归排序左右,再合并 |
| 快速幂 | 本书快速幂教程 | 把指数按二进制拆分 |
经典例题
-
luogu-P1177 排序模板题。实际竞赛建议优先使用
std::sort,本文代码用于理解快速排序原理。 -
luogu-P1923 第 k 小数,适合把快速排序的划分思想改成快速选择。
-
LeetCode 215 快速选择的典型练习。