
本文解析 LeetCode 组合总和(Combination Sum)在 Go 中因切片引用语义导致的常见错误,揭示 append 复用底层数组引发的结果污染问题,并提供安全、可复用的回溯实现方案。
本文解析 leetcode 组合总和(combination sum)在 go 中因切片引用语义导致的常见错误,揭示 `append` 复用底层数组引发的结果污染问题,并提供安全、可复用的回溯实现方案。
你在实现 combinationSum 时观察到一个典型且极易被忽视的 Go 语言陷阱:切片是引用类型,append 可能复用同一底层数组,导致多个结果 slice 指向同一内存区域,后续修改会相互覆盖。
以输入 candidates = [1,2], target = 4 为例,你的递归路径中:
- 当
curComb = [1,1,1](长度 3,容量通常为 4),调用append(curComb, 1)得到[1,1,1,1],存入结果; - 紧接着下一次循环
append(curComb, 2)—— 由于容量足够,Go 直接在原数组第 4 个位置写入2,将[1,1,1,1]覆盖为[1,1,1,2]; - 最终所有保存的
[][]int元素都指向同一块内存,输出变成[[1,1,1,2],[1,1,2],[2,2]],而非预期的[[1,1,1,1],[1,1,2],[2,2]]。
✅ 正确做法:每次找到合法组合时,必须深拷贝当前路径:
import "sort"
func combinationSum(candidates []int, target int) [][]int {
sort.Ints(candidates) // 升序排列,便于剪枝
var result [][]int
backtrack(candidates, target, 0, []int{}, &result)
return result
}
func backtrack(
candidates []int,
target, start int,
path []int,
result *[][]int,
) {
if target == 0 {
// ✅ 关键:创建独立副本,避免引用共享
copyPath := make([]int, len(path))
copy(copyPath, path)
*result = append(*result, copyPath)
return
}
if target < 0 {
return // 剪枝:和已超目标,终止分支
}
for i := start; i < len(candidates); i++ {
// 剪枝:若当前数已大于剩余目标,后续更大数无需尝试
if candidates[i] > target {
break
}
path = append(path, candidates[i])
// 注意:允许重复使用同一元素 → 下一层仍从 i 开始(非 i+1)
backtrack(candidates, target-candidates[i], i, path, result)
path = path[:len(path)-1] // 回溯:撤销选择
}
}? 关键设计说明:
-
start参数控制重复使用:传入i(而非i+1)保证candidates[i]可被多次选取,满足题目“无限次使用”要求; -
排序 + 提前
break:利用升序特性,在candidates[i] > target时直接跳出循环,显著提升效率; - *指针传递 `result`**:避免频繁返回和拼接二维切片,减少内存拷贝开销;
-
显式回溯:
path = path[:len(path)-1]是 Go 中标准的“撤销选择”写法。
⚠️ 补充提醒:
- 不要依赖
fmt.Println(curComb)的输出判断逻辑正确性——打印发生在拷贝前,看到的是“当时”的值,但结果集里存储的已是被后续操作污染的内存; - 若需去重(如
Combination Sum II场景),需在循环内增加去重逻辑:if i > start && candidates[i] == candidates[i-1] { continue }; - 时间复杂度为 $O(N^{\frac{T}{M}})$($N$ 为候选数个数,$T$ 为目标值,$M$ 为最小候选数),空间复杂度为 $O(\frac{T}{M})$(递归栈深度)。
掌握这一切片拷贝原则,不仅解决本题,更是编写健壮 Go 回溯/DFS 算法的基石。

















