快速排序

快速排序先用一个基准值把数组分成“小的在左、大的在右”,再递归排序左右两边。

一句话算法

快速排序先用一个基准值把数组分成“小的在左、大的在右”,再递归排序左右两边。

问题模型

给定一个长度为 nn 的数组,把它按从小到大排序。

快速排序要解决的核心子问题是:

如何在线性时间内,把区间 [l,r] 按某个基准值 pivot 划分成两个更小的区间?

划分完成后:

  • 左边元素都不大于基准值。
  • 右边元素都不小于基准值。

然后分别递归处理左右区间。

核心直觉

快速排序像是在整理一堆卡片:

  1. 先拿一张卡片作为基准。
  2. 把比它小的卡片放左边。
  3. 把比它大的卡片放右边。
  4. 基准附近已经处在正确的相对位置。
  5. 左右两堆继续用同样的方法整理。

它快的原因不是每次只交换相邻元素,而是一次划分就能排除大量跨区间的逆序关系。

算法步骤

双路快速排序

  1. 若区间长度小于等于 1,直接返回。
  2. 选择基准值 pivot,通常取中点或随机位置。
  3. 左指针从左往右找第一个 >= pivot 的元素。
  4. 右指针从右往左找第一个 <= pivot 的元素。
  5. 如果两个指针还没交错,交换它们,然后继续移动。
  6. 指针交错后,递归处理左右两段。

三路快速排序

当数组里有大量重复元素时,普通快速排序会反复处理等于基准值的元素。三路划分把区间直接分成:

< pivot | == pivot | > pivot

中间等于基准值的一整段已经有序,不需要递归。

算法证明

划分不变量:

在双路划分过程中:

  • 左指针左侧已经检查过的元素都 <= pivot
  • 右指针右侧已经检查过的元素都 >= pivot
  • 未检查的元素只在两个指针之间。

每次移动指针都会跳过已经在正确侧的元素;每次交换都会把一对放错侧的元素放回正确侧。因此划分结束后,左段元素都不大于基准值,右段元素都不小于基准值。

递归时,每个子问题都是同样的排序问题,只是区间更短。长度为 01 的区间天然有序。由递归归纳可知,所有子区间最终有序,整个数组也有序。

复杂度分析

一次划分是 O(n)O(n)

  • 平均时间复杂度:O(nlogn)O(n\log n)
  • 最坏时间复杂度:O(n2)O(n^2),例如基准值长期极端偏斜。
  • 额外空间复杂度:平均 O(logn)O(\log n),来自递归栈;最坏 O(n)O(n)

随机选基准或取中点可以降低被有序数据卡到最坏情况的风险。三路快速排序在重复元素很多时更稳。

代码实现

双路快速排序

cpp
        
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; }

三路快速排序

cpp
        
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
大量重复元素排序 本文三路模板 等于基准的一段不再递归

二、快速选择

典型模式: 只需要第 kk 小/第 kk 大,不需要完全排序。 识别信号: 题面问“第 k 小”“中位数”“前 k 个元素”。 核心建模: 划分后只递归包含第 k 个位置的一侧。

应用场景 经典题目 核心思路
第 k 小 luogu-P1923 快速选择只保留一边递归
Top K LeetCode 215 按排名决定递归方向

三、分治思想训练

典型模式: 先把问题按某个标准拆成两边,再分别处理。 识别信号: 问题有明显的“划分后互不干扰”的结构。 核心建模: 找到一个划分操作,使左右子问题规模变小且性质保持一致。

应用场景 经典题目 核心思路
归并排序 排序基础题 先递归排序左右,再合并
快速幂 本书快速幂教程 把指数按二进制拆分

经典例题

  1. luogu-P1177 排序模板题。实际竞赛建议优先使用 std::sort,本文代码用于理解快速排序原理。

  2. luogu-P1923 第 k 小数,适合把快速排序的划分思想改成快速选择。

  3. LeetCode 215 快速选择的典型练习。