快排「三路分流术」:给重复元素开专属车道,告别 O (n²) 堵车
想象一下早高峰的收费站如果只开一条通道轿车、货车、网约车全挤在一起队伍能排到几公里外但如果分三条车道 —— 小型车快速通道、ETC 直达通道、大型车称重通道通行效率立刻翻倍。普通快排就像 “单通道收费站”只分「≤基准」和「≥基准」两条道一旦数组里有大量重复元素它们会全部挤在同一条通道里递归划分越分越歪直接退化成 O (n²) 的 “堵车状态”。而三路划分快排3-way Quicksort就是给排序装上了 “三车道分流系统”把元素分成「小于基准」「等于基准」「大于基准」三股车流其中等于基准的元素直接走 “直达通道”到站就固定位置再也不用参与后续递归排队专治重复元素扎堆的顽疾。 一图速览三路快排思维导图plaintext三路划分快排 ├─ 诞生背景 │ └─ 普通快排遇大量重复元素 → 划分不均 → 时间复杂度退化为O(n²) ├─ 核心思想 │ └─ 数组切分为三段小于基准 / 等于基准 / 大于基准 │ └─ 等于基准的元素一次性定位无需再递归 ├─ 四区间划分 │ ├─ [lo, lt-1]小于基准区左车道 │ ├─ [lt, i-1]等于基准区中间直达道 │ ├─ [i, gt]待处理缓冲区待检区 │ └─ [gt1, hi]大于基准区右车道 ├─ 三指针分工 │ ├─ lt小于区右边界左车道管理员 │ ├─ gt大于区左边界右车道管理员 │ └─ i当前遍历指针巡查员 ├─ 分流规则 │ ├─ 当前元素 基准 → 换入左车道lt、i │ ├─ 当前元素 基准 → 换入右车道gt--、i不动 │ └─ 当前元素 基准 → 归入直达道i ├─ 复杂度 │ ├─ 平均时间O(n log n) │ ├─ 最优场景全重复O(n) │ └─ 空间递归栈平均 O(log n) └─ 适用场景 └─ 含大量重复元素的数组排序二、核心原理三车道分流是怎么工作的我们把数组比作收费站前的车流选取队首元素作为「基准车型」三个 “管理员”指针各司其职指挥车辆分流。1. 车道与角色分工表格车道 / 角色对应区间核心职责左车道小于基准[lo, lt-1]所有比基准小的元素在此排队区间持续向右扩容中间直达车道等于基准[lt, i-1]和基准完全相等的元素进入后直接到站无需再参与后续排序待检缓冲区[i, gt]尚未分类的元素由巡查员逐个检查右车道大于基准[gt1, hi]所有比基准大的元素在此排队区间持续向左扩容lt 管理员左车道终点每进来一辆小车就向后退一步扩大左车道范围gt 管理员右车道起点每进来一辆大车就向前挪一步扩大右车道范围i 巡查员缓冲区起点从第二个元素开始逐个检查指挥元素进入对应车道初始状态lt 站在基准元素位置gt 站在数组末尾i 从基准的下一个元素开始检查。2. 分流指挥规则i 巡查员每遇到一个元素按以下规则调度元素 基准 → 驶入左车道将该元素与 lt 位置的元素交换相当于插入左车道末尾lt 向后移动一位左车道扩容i 继续检查下一个元素。元素 基准 → 驶入右车道将该元素与 gt 位置的元素交换相当于插入右车道开头gt 向前移动一位右车道扩容交换过来的元素未经过检查i 原地不动重新判断。元素 基准 → 走中间直达道无需交换自动归入等于区i 直接向后移动检查下一个。当 i 巡查员与 gt 管理员相遇时本轮分流全部完成。中间直达道的元素位置已永久固定仅需对左右两个车道递归执行分流即可。三、现场演示7 辆车的分流全过程以车队[2, 1, 3, 2, 1, 2, 3]为例基准车型 2 初始状态lt0队首基准gt6队尾i1从第二辆车开始检查i1元素 1 21 号车与 lt 位置的 2 号车交换 → 车队变为[1, 2, 3, 2, 1, 2, 3]lt 退到 1 号位置i 走到 2 号位置i2元素 3 23 号车与 gt 位置的 3 号车交换 → 车队无变化 gt 挪到 5 号位置i 原地待命i2元素 3 23 号车与 gt 位置的 2 号车交换 → 车队变为[1, 2, 2, 2, 1, 3, 3]gt 挪到 4 号位置i 原地待命i2元素 2 2直接归入直达道i 走到 3 号位置i3元素 2 2直接归入直达道i 走到 4 号位置i4元素 1 21 号车与 lt 位置的 2 号车交换 → 车队变为[1, 1, 2, 2, 2, 3, 3]lt 退到 2 号位置i 走到 5 号位置i5 gt4本轮分流结束最终分流结果左车道[0,1][1,1]需继续递归分流中间直达道[2,4][2,2,2]全部到位无需再处理右车道[5,6][3,3]需继续递归分流四、代码实现带 “分流注释” 的 C 版本加入随机选基准优化避免有序数组触发最坏情况关键代码对应分流角色一目了然。cpp运行#include iostream #include vector #include cstdlib #include ctime using namespace std; // 三路分流核心递归函数 void quickSort3Way(vectorint cars, int lo, int hi) { if (lo hi) return; // 车队为空或仅1辆车无需分流 // 随机抽取基准车型并换到队首规避有序车队最坏情况 int pivotIdx lo rand() % (hi - lo 1); swap(cars[lo], cars[pivotIdx]); int pivot cars[lo]; int lt lo; // 左车道管理员小于区右边界 int gt hi; // 右车道管理员大于区左边界 int i lo 1;// 巡查员当前待检查车辆 while (i gt) { if (cars[i] pivot) { // 车型更小交换到左车道末尾 swap(cars[lt], cars[i]); lt; // 左车道扩容 i; // 继续检查下一辆 } else if (cars[i] pivot) { // 车型更大交换到右车道开头 swap(cars[i], cars[gt]); gt--; // 右车道扩容 // i 不动交换过来的车辆还未检查 } else { // 车型一致直接走中间直达道 i; } } // 仅递归分流左右车道中间直达道全部就位跳过 quickSort3Way(cars, lo, lt - 1); quickSort3Way(cars, gt 1, hi); } // 对外调用接口 void quickSort(vectorint arr) { srand((unsigned)time(nullptr)); // 初始化随机种子 quickSort3Way(arr, 0, arr.size() - 1); } // 测试示例 int main() { vectorint arr {3,1,4,1,5,9,2,6,5,3,5}; quickSort(arr); for (int num : arr) { cout num ; } return 0; }五、性能总结什么时候用它最划算时间效率平均场景与普通快排持平为 O (n log n)仅多少量比较开销最优场景数组全为重复元素时一次划分完成排序时间复杂度低至 O (n)最坏场景极端不均匀分布下为 O (n²)随机选基准后出现概率极低工程中可忽略空间开销仅递归调用栈占用空间平均 O (log n)适用场景数组包含大量重复元素时性能显著优于普通两路快排元素几乎无重复时性能与普通快排接近属于 “稳赚不亏” 的优化是工业界排序算法的经典优化方案很多语言的内置排序都借鉴了该思想谢谢