go回溯首选闭包捕获res/path以避免参数爆炸,但需深拷贝path防共享底层数组;剪枝应写在for终止条件而非循环体内以省开销;终止条件依问题而异:组合/排列看len(path),子集每层都收集,combinationsum则据sum与target关系判断。

回溯函数怎么写:用闭包还是独立函数?
Go 里写回溯,最自然的方式是把 backtracking 写成闭包,直接捕获外部的 res 和 path。这不是偷懒,而是避免传参爆炸——尤其当状态变量多于 3 个(比如加了 used 数组、sum 累加值、target 剩余值)时,独立函数签名会迅速失控。
但要注意:闭包里修改 path 是在共享底层数组,所以每次加入结果前必须深拷贝。常见错误是直接 res = append(res, path),结果所有元素最后都变成空切片或最后一组值。
- 正确做法:
tmp := make([]int, len(path)); copy(tmp, path); res = append(res, tmp) - 别用
slices.Clone(path)(Go 1.21+)替代copy,它底层也是make + copy,但显式写出来更可控、更易 debug - 如果
path是字符串或结构体字段,也要注意是否含指针或 map —— 深拷贝不能只靠copy
剪枝写在哪:for 循环条件里还是循环体内?
剪枝不是锦上添花,是防止超时的关键。比如组合问题 combine(n, k),不剪枝时复杂度是 O(n^k),剪枝后降到 O(C(n,k))。真正的剪枝逻辑应该压进 for 的终止条件,而不是塞在循环体开头 if 判断里。
原因很简单:后者仍会进入递归栈帧再退出;前者连循环都不启动,省掉分配、调用、回退三层开销。
- 典型写法:
for i := start; i - 这个公式意思是:“剩下还要选
k - len(path)个数,那当前最多只能从位置n - (k - len(path)) + 1开始选,否则后面不够填” - 别硬记公式,每次写完在纸上画一棵小 N 叉树,标出第几层、还剩几个要选、当前最大可选下标,推一遍就清楚了
路径撤销为什么总忘:path = path[:len(path)-1] 的坑
回溯的核心动作就是“试—错—撤”,而“撤”这一步最容易漏或写错。最常见的是写成 path = path[0:len(path)-1] 或 path = path[:len(path)-2],导致 panic 或逻辑错位。
- 永远用
path = path[:len(path)-1]—— 它语义清晰:切掉最后一个元素,长度减一 - 别用
path = path[0:len(path)-1],虽然效果一样,但0:多余且干扰阅读 - 如果用了
append(path, x)后没及时撤销,下一轮递归会带着上轮残留数据跑,输出结果重复、错乱甚至 panic(比如越界访问) - 调试时可在
backtracking入口打日志:fmt.Printf("enter: %v, len=%d\n", path, len(path)),一眼看出哪层没撤干净
递归终止条件怎么设:用 len(path) == k 还是 i > n?
终止条件不是“走到头才停”,而是“目标达成就收手”。对组合、子集、排列这类构造型问题,判断依据永远是路径长度或状态满足性,而不是索引越界。
比如 subsets(nums),你不会等 i == len(nums)+1 才收集结果,而是在每层递归开头就 append 当前 path —— 因为子集本质就是所有中间态。
-
combine/permute/letterCombinations:终止条件是len(path) == targetLen -
subsets:无显式终止条件,靠i == n自然结束,但收集动作放在递归入口 -
combinationSum类带约束的问题:终止条件可能是sum > target(剪枝退出)或sum == target(成功收集)
混淆这两类会导致逻辑混乱:该收集时不收,不该收时狂收,或者递归永不退出。
回溯最难的不是写对第一版,而是改需求时能快速定位哪一行控制“什么时候停”、哪一行控制“能不能进”。把终止和剪枝逻辑分开、命名清晰(比如叫 shouldStop、canProceed),比堆砌技巧更重要。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











