快速排序(QuickSort)的效率和划分位置(pivot)选择关系非常大。选得不好会退化成 O(n2)O(n^2),选得合理则能保证平均 O(nlogn)O(n \log n)。
🔹 常用的几种划分位置(pivot)选择方法
1. 固定位置法
- 取第一个元素 / 最后一个元素 / 中间元素 作为 pivot。
- 优点:实现最简单。
- 缺点:当输入数组本身接近有序时,极易退化成 O(n2)O(n^2)。
int pivot = nums[left]; // 取最左边
// 或
int pivot = nums[right]; // 取最右边
2. 随机选择法(Randomized QuickSort)
- 在
[left, right]区间随机选一个元素作为 pivot。 - 优点:能有效避免最坏情况(比如已经有序的数据)。
- 缺点:有少量随机数开销,但可忽略。
Random rand = new Random();
int pivotIndex = left + rand.nextInt(right - left + 1);
swap(nums, pivotIndex, right); // 把 pivot 换到最后
3. 三数取中法(Median-of-Three)
- 取区间的 左端、右端、中间 三个元素,取它们的中位数作为 pivot。
- 优点:能较好避免最坏情况,减少递归深度。
- 缺点:需要额外比较操作,但提升稳定性。
int mid = left + (right - left) / 2;
int pivot = medianOfThree(nums[left], nums[mid], nums[right]);
4. 九数取中法 / 更高阶采样
- 类似三数取中,但取更多点(比如 9 个点),再递归找中位数。
- 优点:分区更均衡。
- 缺点:开销大,通常用于大规模排序。
5. Median-of-Medians(中位数的中位数)
-
线性时间 O(n)O(n) 选择 pivot 的方法。
-
过程:
- 将数组分成若干组(一般 5 个一组)。
- 每组找中位数。
- 把这些中位数组成新数组,递归找它们的中位数。
- 得到的中位数作为 pivot。
-
优点:保证最坏情况也是 O(nlogn)O(n \log n)。
-
缺点:实现复杂,常用于 QuickSelect(找第 k 大/小)。
🔹 对比总结
| 方法 | 平均复杂度 | 最坏复杂度 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 固定位置(左/右/中) | O(nlogn)O(n \log n) | O(n2)O(n^2) | ★ | 简单场景 |
| 随机选择 | O(nlogn)O(n \log n) | 很难退化成 O(n2)O(n^2) | ★★ | 常用,实际工程 |
| 三数取中 | O(nlogn)O(n \log n) | 接近均衡,避免 O(n2)O(n^2) | ★★ | 常用优化 |
| 九数取中 | O(nlogn)O(n \log n) | 很稳定 | ★★★ | 大规模排序 |
| Median-of-Medians | O(nlogn)O(n \log n) | O(nlogn)O(n \log n) | ★★★★ | 特殊场景,理论保证 |
✅ 在 实际应用 中:
- 随机选择 和 三数取中 最常用;
- Median-of-Medians 多用于 选择问题(QuickSelect) ,而不是普通排序。
好问题 👍 我来详细解释一下你写的这段随机数代码为什么要这么取随机数,以及逻辑背后的原因:
Random rand = new Random();
int pivotIndex = left + rand.nextInt(right - left + 1);
swap(nums, pivotIndex, right); // 把 pivot 换到最后
1. rand.nextInt(right - left + 1) 的含义
rand.nextInt(n)会生成 [0, n) 之间的整数(包含 0,不包含 n)。- 所以
rand.nextInt(right - left + 1)会生成[0, right-left]之间的整数。 - 再加上
left,范围就变成了 [left, right] ,也就是当前区间里任意一个下标。
这样就能 随机选择当前区间的任意一个元素作为 pivot(基准值) 。
2. 为什么要随机选 pivot?
快速排序的核心步骤是 选取 pivot 并划分,不同的 pivot 会导致性能差异很大:
- 坏情况(最差 O(n²)) :
如果每次都选到最小值/最大值,划分特别不平衡,比如[1,2,3,4,5]已排序数组,选第一个元素当 pivot,就会退化成冒泡/选择排序。 - 好情况(平均 O(n log n)) :
如果 pivot 能把区间均匀划分(接近中位数),那么划分比较平衡,递归树深度接近 log n,总复杂度就是 O(n log n)。
👉 随机选 pivot 能 打破输入的特殊性,使得算法的期望复杂度稳定在 O(n log n),大大降低退化风险。
3. 为什么把 pivot 换到最后?
swap(nums, pivotIndex, right);
- 在 partition 操作里,通常我们以最后一个元素作为 pivot 来写逻辑(简洁:遍历 [left, right-1],最后把 pivot 放回正确位置)。
- 如果随机选到别的下标,就先和
right位置交换,这样 partition 的代码不用改动,依旧是以nums[right]为基准。
这样做的好处是:
- partition 逻辑更统一(永远用末尾元素当 pivot)。
- 实现简洁,不用额外处理 pivotIndex。
✅ 总结:
rand.nextInt(right - left + 1)→ 生成 [left, right] 范围的随机下标。- 随机选 pivot → 避免退化情况,保证期望 O(n log n)。
- swap 到最后 → 简化 partition 代码逻辑。
1. Java 里 nextInt() 的定义
在 Java 里,Random 的用法是这样的:
rand.nextInt(n) // 生成 [0, n) 的随机整数
⚠️ 注意:参数 n 必须是一个整数(生成 0 到 n-1 之间的随机数)。
也就是说,Java 本身 没有 rand.nextInt(left, right+1) 这样的双参数形式(至少在旧版本里没有)。
2. 为什么写成 left + rand.nextInt(right - left + 1)
因为我们需要在 [left, right] 范围里随机取一个整数,
- 区间长度是
(right - left + 1) - 在这个区间里取一个 偏移量(0 到区间长度-1)
- 再加上
left作为起点,就变成了[left, right]
所以写法是:
int pivotIndex = left + rand.nextInt(right - left + 1);
3. Java 17 以后确实有类似功能
在 Java 17 开始,Random 新增了一个重载:
rand.nextInt(origin, bound) // [origin, bound) 左闭右开
所以你可以写成:
int pivotIndex = rand.nextInt(left, right + 1);
这时候效果和 left + rand.nextInt(right - left + 1) 是一样的,因为 right+1 保证了取值范围是 [left, right]。
✅ 总结:
- 旧版本 Java:只能写
left + rand.nextInt(right - left + 1) - 新版本 Java(17+):可以直接写
rand.nextInt(left, right+1),更直观