归并排序稳定、可预测、适合多路合并;快速排序平均快但最坏退化、不稳定、对pivot敏感;sort.Ints不用归并因需兼顾小数组插入排序、中等规模三数取中快排、大数组堆排降级,避免归并的O(n)空间与高cache miss。

直接说结论:归并排序稳定、可预测、适合多路合并场景;快速排序平均快但最坏退化、不稳定、对 pivot 敏感——选哪个不看“理论复杂度”,而看你要保序、抗退化,还是拼平均吞吐。
为什么 sort.Ints 底层不用归并排序
Go 的 sort.Ints 不用归并排序,是因为它要兼顾小数组、部分有序、内存局部性等工程现实:
– 长度 ≤12 时走 insertionSort,比归并的递归开销和临时空间更轻;
– 中等长度用 quickSort + 三数取中 doPivot,避免已排序输入退化;
– 大数组且递归过深时 fallback 到 heapSort,保证 O(n log n) 上界;
– 归并排序虽稳定且理论均衡,但必须分配 O(n) 临时空间,cache miss 高,在小数据或高频调用场景下反而更慢。
归并排序的 merge 实现最容易错在哪
核心不是分治逻辑,而是 merge 合并时边界处理和剩余段拷贝:
– 忘记处理左/右子数组某一方提前耗尽的情况,导致漏拷贝;
– 用 copy(nums[l:r+1], tmp) 时,tmp 长度必须严格等于 r-l+1,否则 panic 或静默截断;
– 把 mid 算成 (l+r)/2 而非 l + (r-l)>>1,在大整数下可能溢出(虽在 int32/64 下少见,但不符合 Go 标准写法);
– 递归切片时写成 nums[:len(nums)/2] 和 nums[len(nums)/2:],看似简洁,但若原切片有 cap 剩余,会意外共享底层数组,引发并发或修改污染。
快速排序的 pivot 选择为什么不用 rand
Go 的 sort 包所有 pivot 选择都是确定性的,原因很实际:
– doPivot 固定取首、中、尾三数的中位数,不依赖 math/rand;
– 测试可重现:同一输入在任何机器、任何 Go 版本下行为一致;
– 避免因随机性掩盖逻辑 bug(比如 partition 漏元素、边界越界);
– 不引入 seed 初始化成本,也不需担心并发调用时 rand 全局状态冲突;
– 对近似有序输入,三数取中比纯随机更大概率避开最差 pivot,把递归深度控制在 ~2×log₂n 内,防止栈溢出。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
什么时候该手写归并而不是用 sort.Stable
sort.Stable 是通用接口,包装 []int 需额外类型转换,且内部仍走分组插入+对称归并,未必匹配你的数据特征:
– 你要合并多个已排序切片(如日志按时间分片写入),手写归并可直接多路 merge,省掉先 concat 再重排的内存和时间;
– 你有自定义比较逻辑且涉及 I/O(如磁盘归并排序),需要控制 buffer 大小和 chunk 边界;
– 你在做外部排序或流式处理,无法一次性加载全部数据到内存;
– 你明确知道数据规模极小(sort.Stable 的接口抽象和反射开销——此时手写 inline 归并反而更干净。
真正难的不是写出能跑通的版本,而是判断“当前这个排序,到底要不要自己管”。Go 的 sort 包已经把大多数 case 封装好了,但一旦涉及稳定性、多路、流式、内存受限,你就得亲手碰 merge 的指针和 partition 的边界。这些地方,错一个 就是静默错误,不是 panic。

















