Go手写快排核心是原地partition而非分治逻辑;禁用append拼接因会触发底层数组扩容,导致原切片不变、空间复杂度升至O(n²);pivot索引须传low/high边界,避免空切片时len(arr)-1越界panic;终止条件应为low>=high。

Go 里手写快速排序,核心不是“怎么分治”,而是“怎么原地分区”——不理解 partition 的边界控制和 pivot 归位,所有递归都是空中楼阁。
为什么不能用 append 拼左右子切片?
新手最常写的错误是:left := []int{}、right := []int{}、再用 append 收集元素,最后拼成新切片返回。这看似逻辑清晰,实则彻底违背 Go 切片语义:
- 每次
append都可能触发底层数组扩容,生成全新内存块,原切片变量完全不受影响 - 你传进来的
arr是个引用头(指针+长度+容量),但新建切片后排序结果只存在临时副本里,调用方根本看不到变化 - 空间复杂度从 O(log n) 崩溃到 O(n²),递归深度大时直接爆内存
- 无法和后续操作(如
sort.Search)共享同一份有序数据,失去可控性
partition 函数里 pivot 下标为什么不能写 len(arr)-1?
这是运行时 panic 的高发点。原因很简单:len(arr) 是当前切片长度,而递归中传入的往往是子切片(比如 arr[lo:hi]),它可能为空(len(arr) == 0),此时 len(arr)-1 就是 -1 —— 直接越界 panic。
正确做法必须显式传入区间边界:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 函数签名应为
partition(arr []int, low, high int),而非只收arr []int - 取 pivot 时用
arr[high](Lomuto)或arr[low](Hoare),前提是已确认low - 开头加守卫:若
low > high或len(arr) == 0,直接返回,不碰任何索引
递归终止条件必须是 low >= high,不是 len(arr)
很多教程写 if len(arr) ,这在递归调用子切片时会失效。因为 <code>len(arr) 是当前切片长度,而你的逻辑始终在 [low, high] 子区间内操作 —— high 可能远小于 len(arr)-1。
典型错误后果:
if len(arr) 在第一次递归就跳过(原始切片长 10,但传入 <code>arr[0:5],len是 5,不满足)→ 看似正常,但边界混乱-
if low == high漏掉空区间(low > high),某些 pivot 选择下会导致无限递归 - 正确且鲁棒的写法只有:
if low >= high { return }—— 它同时覆盖 0 个元素(low > high)和 1 个元素(low == high)两种终止态
要不要随机选 pivot?先看场景
选最后一个(arr[high])不是最优,而是最安全:边界清晰、无额外计算、不引入 rand 依赖。但真实业务中,如果输入可能接近有序(比如日志时间戳、ID 自增),固定选首/尾会让快排退化成 O(n²)。
这时才需要干预:
- 简单加固:在
partition开头加随机交换,randIndex := low + rand.Intn(high-low+1),再arr[randIndex], arr[high] = arr[high], arr[randIndex] - 注意:必须提前
rand.Seed(time.Now().UnixNano()),否则每次种子一样,随机失效 - 更进一步可加小数组优化(
high-low 时切插入排序),但日常 <code>sort.Slice已内置该策略,手写仅限算法题或定制需求
真正容易被忽略的是:无论是否随机,partition 必须准确返回 pivot 的**最终索引**;这个值要直接用于下一层递归的区间划分,错一位就会漏排或越界 —— 它不是“大概中间”,而是交换完成后 pivot 真实落定的位置。

















