这个up主代码分析的很直白,浅显易懂,但是代码优化不够,可以作为参考
https://www.bilibili.com/video/BV1vP411g7J3/?spm_id_from=333.337.search-card.all.click
一、普通快排:分治思想与复杂度
快速排序的总体思路是「分而治之」。每一轮先选择一个基准元素 pivot,然后通过划分操作,把小于 pivot 的元素放到左侧,把大于 pivot 的元素放到右侧,pivot 最终落位。接下来对左右两个子区间分别递归执行同样的操作,直到子区间只剩一个元素或为空。
由这个递归树可以看到,如果每一轮划分都能把序列近似对半切开,递归深度约为 log n,每一层总体的比较和交换开销约为 O(n),因此最好情况和平均情况的时间复杂度为 O(n log n)。但快速排序并不是稳定的原地划分,当划分严重不均衡时,递归深度会接近 n,最坏时间复杂度会退化到 O(n²)。

基础二路快排实现
下面是一段常见的二路划分快排代码,其中已经加入了随机 pivot,为了防止是有序数组的情况下快排时间复杂度变成O(n2):
cpp
class Solution {
public:
int patition(vector<int>& a, int low, int high)
{
int idx = low + rand() % (high - low + 1);
swap(a[low], a[idx]);
int pivot = a[low];
while (low < high)
{
while (low < high && a[high] >= pivot)
{
high--;
}
a[low] = a[high];
while (low < high && a[low] <= pivot)
{
low++;
}
a[high] = a[low];
}
a[low] = pivot;
return high;
}
void qwe(vector<int>& nums, int low, int high)
{
if (low < high)
{
int pos = patition(nums, low, high);
qwe(nums, low, pos - 1);
qwe(nums, pos + 1, high);
}
}
vector<int> sortArray(vector<int>& nums)
{
qwe(nums, 0, nums.size() - 1);
return nums;
}
};
二、有序输入为什么会退化
如果输入本身已经有序,并且每次固定选择最左端元素作为 pivot,那么划分会出现非常不均衡的情况:pivot 已经把整个序列扫描了一遍,结果却只切出一个元素,剩下的元素仍然留在同一侧。于是递归实际上变成了每次只减少一个元素,递归深度接近 n。
在这种情况下,每一层的扫描长度依次是 n、n-1、n-2......,总代价约为 1 加到 n,也就是 O(n²)。所以快排最怕「已经有序的序列 + 固定选端点为 pivot」这种组合。
我们暂时把代码中的随机 pivot 去掉,固定选择第一个元素:
int pivot = a[low];
假设数组已经有序:
[1, 2, 3, 4, 5, 6, 7]
第一次选择 1 作为 pivot。
[1] [2, 3, 4, 5, 6, 7]
↑ ↑
pivot 下一次递归
由于 1 是最小值, 这段代码:
while(low < high && a[high] >= pivot)
{
high--;
}
会让 high 从最右端一直移动到最左端。
最后 pivot 被放回原位。
cpp
[1] [2, 3, 4, 5, 6, 7]
↑ ↑
pivot 下一次递归
本来希望分成 3 个和 3 个,现在却分成了 0 个和 6 个。
接着递归右边:
cpp
第一次:[1] [2, 3, 4, 5, 6, 7]
第二次:[2] [3, 4, 5, 6, 7]
第三次:[3] [4, 5, 6, 7]
第四次:[4] [5, 6, 7]
第五次:[5] [6, 7]
第六次:[6] [7]
每次只能确定一个元素的位置。
所以总操作量约为:
cpp
(n-1)+(n-2)+......+1
这是等差数列:
cpp
n(n-1)/2
因此时间复杂度退化成:
O(n2)
这就是有序数组会让固定首元素 pivot 的快速排序退化的根本原因。
三、随机 pivot 的作用(我的理解是破坏有序数组)与局限
针对有序输入退化,一个简单有效的优化是随机选择 pivot:在 low 到 high 范围内随机取一个下标,再把这个位置上的元素换到最左端,继续使用原来的划分逻辑。
这样做之后,输入是否有序不再直接决定 pivot 的位置,划分更有可能接近均匀的期望效果,平均复杂度稳定在 O(n log n)。上面的第一段代码就通过「low + rand() % (high - low + 1)」实现了随机 pivot。
不过随机 pivot 也有局限:它降低的是最坏情况出现的概率,并不能从理论上消除最坏情况;同时,对于
大量重复元素的场景,仅靠随机 pivot 仍然可能退化,因为问题已经不在 pivot 的选择上,而在划分方式上。
四、大量重复元素为什么也会退化
普通二路划分通常把元素分成「小于等于 pivot」和「大于等于 pivot」两类。当数组中存在大量与 pivot 相等的元素时,这些相等元素既可能被分到左边,也可能被分到右边,划分结果变得非常不均衡:相等的元素没有形成一个可以确定不动的整体,很多相等元素还会继续进入下一层递归。
于是,即使选择了随机 pivot,大量重复元素仍然会让递归区间难以快速收敛,严重时时间复杂度退化到 O(n²)。要解决这个问题,需要把等于 pivot 的部分单独划成一个区间,这正是三路划分的思想。
五、三路划分:lt、i、gt 指针过程

三路划分把数组分成三个部分:小于 pivot、等于 pivot、大于 pivot。这里使用三个指针来维护区间边界:
- lt:表示小于 pivot 区间的右边界。区间 low, lt-1 中的元素都小于 pivot。
- i:当前正在检查的元素位置。区间 lt, i-1 中的元素都等于 pivot。
- gt:表示大于 pivot 区间的左边界。区间 gt+1, high 中的元素都大于 pivot。
算法从 i = low + 1 开始扫描,直到 i 超过 gt:
- 如果 numsi 小于 pivot,交换 numslt 和 numsi,然后 lt 和 i 同时右移。
- 如果 numsi 大于 pivot,交换 numsi 和 numsgt,然后 gt 左移;注意此时 i 不前进,因为交换过来的是一个新的未检查元素。
- 如果 numsi 等于 pivot,只让 i 右移。
一轮结束后,low, lt-1 是小于区,lt, gt 是等于区,gt+1, high 是大于区。等于区已经确定且不需要参与后续递归,只需要递归处理小于区和大于区即可。
六、完整 C++ 三路快排实现
以下是结合随机 pivot 和三路划分的完整 C++ 实现:
cpp
class Solution {
public:
void quickSort(vector<int>& nums, int low, int high)
{
if (low >= high)
return;
// 随机选择 pivot
int idx = low + rand() % (high - low + 1);
swap(nums[low], nums[idx]);
int pivot = nums[low];
int lt = low; // [low, lt-1] < pivot
int i = low + 1; // [lt, i-1] == pivot
int gt = high; // [gt+1, high] > pivot
while (i <= gt)
{
if (nums[i] < pivot)
{
swap(nums[lt], nums[i]);
lt++;
i++;
}
else if (nums[i] > pivot)
{
swap(nums[i], nums[gt]);
gt--;
}
else
{
i++;
}
}
// 等于 pivot 的部分不需要递归
quickSort(nums, low, lt - 1);
quickSort(nums, gt + 1, high);
}
vector<int> sortArray(vector<int>& nums)
{
if (!nums.empty())
{
quickSort(nums, 0, (int)nums.size() - 1);
}
return nums;
}
};
七、复杂度对比表
把前面讨论的几种情况汇总如下:
| 场景 | 时间复杂度 | 说明 |
|---|---|---|
| 最好情况、平均情况 | O(n log n) | 每次划分比较接近均衡,递归深度约为 log n |
| 最坏情况(有序序列 + 固定首元素 pivot) | O(n²) | 每次仅划分出一个元素,递归深度接近 n |
| 随机 pivot 平均情况 | O(n log n) | 降低有序输入导致退化的概率 |
| 大量重复元素 + 普通二路划分 | O(n²) | 相等的元素无法被有效分到两侧,递归迟迟不收敛 |
| 三路划分(含大量重复元素) | 平均 O(n log n),大量重复时接近 O(n) | 等于 pivot 的区间一次确定,不参与后续递归 |