Go标准库未提供双轴快排,需手动实现:选首尾两pivot划分三区间,用三指针原地分区,递归排序左右段而跳过中间段;注意p1==p2退化、索引边界及pivot位置避让。

双轴快速排序在 Go 中不是标准库函数
Go 标准库 sort.Slice 和 sort.Ints 底层用的是 pdqsort(混合了 introsort、堆排序和插入排序的变体),**不暴露双轴(dual-pivot)逻辑,也不提供接口让你指定 pivot 数量**。所以“实现双轴快速排序”本质上是手动写一个非标准的、基于两个 pivot 的分治排序——它不是对 sort 包的调用优化,而是另起炉灶。
双轴分区的核心逻辑:三个区间划分
单轴快排把数组分成「 pivot」两块;双轴快排选两个 pivot(通常取首尾或首中尾中的两个),比如 p1 ,然后一趟扫描把数组划成三段:
-
[low, lt):所有 p1 的元素 -
[lt, gt]:所有 ∈ [p1,p2] 的元素(含等于) -
(gt, high]:所有 >p2的元素
关键点在于中间段不用再递归——这减少了子问题数量,尤其对含大量重复值的数组更友好。实际编码时常用三指针:lt(左边界右扩)、gt(右边界左缩)、i(当前扫描位)。常见错误是没处理好 p1 == p2 时的退化,导致无限循环或 panic。
Go 中实现双轴快排必须注意切片与原地性
Go 切片传递的是 header(指针+长度+容量),所以只要不重新分配底层数组,就能原地排序。但双轴分区扫描中频繁交换元素,容易写错索引边界。下面这个片段是安全起点:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func dualPivotQuickSort(arr []int, low, high int) {
if low >= high {
return
}
p1, p2 := arr[low], arr[high]
if p1 > p2 {
p1, p2 = p2, p1
arr[low], arr[high] = p2, p1 // 同步调整位置
}
<pre class="brush:php;toolbar:false;">lt, gt := low+1, high-1
i := lt
for i <= gt {
if arr[i] < p1 {
arr[lt], arr[i] = arr[i], arr[lt]
lt++
i++
} else if arr[i] > p2 {
arr[i], arr[gt] = arr[gt], arr[i]
gt--
// i 不动:刚换过来的 arr[i] 还没检查
} else {
i++
}
}
// 把 p1 放到 lt-1,p2 放到 gt+1
arr[low], arr[lt-1] = arr[lt-1], arr[low]
arr[high], arr[gt+1] = arr[gt+1], arr[high]
dualPivotQuickSort(arr, low, lt-2) // 左段:< p1
dualPivotQuickSort(arr, lt, gt) // 中段:∈ [p1,p2],已有序,跳过
dualPivotQuickSort(arr, gt+2, high) // 右段:> p2}
注意:lt 初始为 low+1,gt 初始为 high-1,避免把 pivot 自身卷入扫描;最后三段递归的下标要严格避开 pivot 位置,否则会重复排序或越界。
性能提升只在特定数据分布下明显
双轴快排的理论优势是减少比较次数(尤其对重复值多、范围小的数据),但 Go 的 runtime 对小数组(sort.Ints:
- 数据量大(> 10⁵)且含大量重复值(如日志时间戳、状态码)
- 内存敏感,不能接受归并排序的 O(n) 额外空间
- 已排除 pdqsort 的 fallback 路径干扰(比如禁用其 introsort 检测)
真正值得投入优化的点,往往不在 pivot 数量,而在:是否提前用 sort.IsSorted 检查已序、是否对小切片用 insertionSort 手动兜底、是否用 unsafe.Slice 绕过 bounds check(仅限极端场景)。


















