应使用[]rune而非[]byte排序字符串,因[]byte按字节排序会破坏UTF-8编码导致乱序或panic;[]rune按Unicode码点排序语义正确,且支持原地交换与引用传递,partition需显式传入low、high边界防越界panic。

别用 sort.Slice 就别手写——除非你明确需要控制 pivot 选择、分区逻辑,或处理非 []int 类型但又不想引入泛型约束的 rune 切片。
为什么不能直接对 []byte 做快速排序?
Go 中字符串是 UTF-8 编码的字节序列,[]byte 排序会按字节值比较,导致中文、emoji 或带重音符号的字母(如 “é”)被错误拆分和乱序。比如 "café" 转成 []byte 是 [99 97 195 169],排序后变成 [97 99 169 195],再转回 string 就不是合法 UTF-8,甚至 panic。
正确做法是转成 []rune:每个 rune 对应一个 Unicode 码点,排序时按码点值比较,语义正确。
-
[]rune支持原地交换:r[i], r[j] = r[j], r[i]安全有效 - 长度与原始字符串字符数一致,不会因多字节而膨胀
- 切片传递仍是引用语义,递归子切片共享底层数组
partition 函数里必须传 low 和 high,不能依赖 len(r)
常见错误是在 partition 开头写 pivot := r[len(r)-1] —— 当递归传入空切片(如 r[lo:lo])时,len(r) == 0,len(r)-1 就是 -1,直接 panic。
立即学习“go语言免费学习笔记(深入)”;
正确签名必须带边界参数:
func partition(r []rune, low, high int) int {
if low >= high {
return low
}
pivot := r[high]
i := low
for j := low; j < high; j++ {
if r[j] <= pivot {
r[i], r[j] = r[j], r[i]
i++
}
}
r[i], r[high] = r[high], r[i]
return i
}
-
low和high是闭区间索引,调用方负责保证low - 循环用
j ,不包含 pivot 本身,避免自比 - 返回前必须执行一次 swap,确保 pivot 归位
递归调用必须用切片子表达式,不能 append 或新建切片
有人想“构造左右两段再拼起来”,写成:
left := append([]rune{}, r[low:i]...)
right := append([]rune{}, r[i+1:high+1]...)
// …然后试图合并——这完全失效
后果是:原切片未被修改;新分配内存;GC 压力大;空间复杂度从 O(log n)(栈深)暴涨到 O(n²)。
正确方式只操作原切片的子视图:
func quickSort(r []rune, low, high int) {
if low >= high {
return
}
p := partition(r, low, high)
quickSort(r, low, p-1) // 左半段:[low, p-1]
quickSort(r, p+1, high) // 右半段:[p+1, high]
}
-
r[low:p]和r[p+1:high+1]共享底层数组,所有交换都生效 - 注意右边界是
high+1,因为切片右开,r[p+1:high+1]才覆盖到high - 空区间自动被
if low >= high拦住,不会进入 partition
实际调用时要先转 []rune,再还原为 string
原地排序只作用于 []rune,最终需转回 string。注意不要漏掉类型转换:
s := "你好 world" rs := []rune(s) quickSort(rs, 0, len(rs)-1) sorted := string(rs) // ✅ 正确 // ❌ 错误:s = string(rs) 但没赋给变量,原 s 不变
- 函数不能接收
string直接排序,因为 string 是只读的 - 如果只是临时排序(如用于去重、二分查找),保持
[]rune更高效 - 若需频繁排序同一批字符串,可封装成函数,避免重复转换
最易被忽略的是:分区函数里 i 的初始值、j 的上界、以及 pivot 归位那一次 swap —— 少一个,结果就错一半,且很难 debug。写完务必用 ["a", "z", "m"] 和 ["中", "文", "字"] 两类 case 测一遍。


















