子集:每个递归入口都是答案,先拷贝
走进 dfs(idx) 时,手上的 path 已经是一个合法子集。先拷一份放进结果,再从 idx 往后选下一个数,选完就撤。
dfs(idx) 入口先把 path 的副本写入结果,再对 i=idx..n-1:把 nums[i] 加入 path,递归 dfs(i+1),弹出。每个入口对应一个子集,共 2^n 个;必须拷贝,因为 path 会被后续修改。时间 O(n·2^n),空间 O(n)。
给你一个不含重复数字的整数数组,返回它的所有子集。子集不讲究顺序,空集也要算进去。结果里不能有重复的子集。
主例 nums = [1, 2, 3]。八个子集是 []、[1]、[2]、[1,2]、[3]、[1,3]、[2,3]、[1,2,3]。少一个都不对;把 [1,2] 和 [2,1] 当成两份也对不上题目「集合」的定义。
本文要回答:为什么不是走到叶子才记一次,以及 path 不拷贝、只存引用,主例的结果会变成什么样。
选与不选,两条路
每个元素都可以进当前子集,也可以不进,n 个数就是 2^n 个子集。可以画成一棵二叉树:在下标 i 处分「选 i」和「不选 i」,走到 n 才记路径。代码用另一种走法,少一层叶子判断:每一个递归入口,当前 path 本身就已经是一个答案——还没选后面的数,等于后面的数全部「不选」。缺的不是决策,而是「现在就记下来」。
dfs(idx) 的第一件事:把 path 拷一份推进结果。空路径进来时,记下 []。然后从 i=idx 循环到末尾:选择 nums[i] 加入 path,递归 dfs(i+1)——下一个只能从 i 后面挑,避免 [1,2] 和 [2,1] 各生成一次。递归返回后把刚才加入的数弹掉,让下一轮循环站在同一条旧路径上选另一个数。
必须拷贝。path 是同一块底层数组,后面 append、截断都会改它。若把 path 本身放进结果,主例走到 [1,2,3] 时,前面记下的那些「子集」会全部变成同一段被改过的切片,最后往往只剩几份相同的尾状态。append([]int{}, path...) 是新开一段,把当前内容冻住。
用手走主例。dfs(0)、path=[]:先记下 []。i=0 选 1,进入 dfs(1)、path=[1],记下 [1];再选 2,进入 dfs(2)、path=[1,2],记下 [1,2];再选 3,进入 dfs(3)、path=[1,2,3],记下 [1,2,3],循环结束。弹 3,dfs(2) 的循环也结束;弹 2,回到 path=[1],选 3,记下 [1,3]。弹 1,回到空路径,选 2,记下 [2],再扩出 [2,3];最后选 3,记下 [3]。八份,一份不漏。
演示场景按「当前处理到哪个数、已经收下哪些子集」往前推,收官是这八个。和「每个元素选或不选」数出来的集合相同,只是记录发生在每个入口,而不是只在走到 n 的时候。nums 为空,dfs(0) 仍会先拷一份空路径,结果是 [[]],符合题意。
时间是每个子集拷一次,每次拷 O(n),一共 O(n·2^n)。递归深度最多 n,path 最多 n 个数。有重复元素时这套会生成重复子集,那是 LC90,要先排序再跳过相同的 i。
入口即答案dfs(idx) 被调用时,path 表示「已经选定的数」,后面的数都还没选。这个状态本身就是一个子集。先拷贝再扩展,空集、单元素、全套都落在同一句话里,不必单独处理叶子。
Go:选/不选回溯
func subsets(nums []int) [][]int {res := [][]int{}path := []int{}var dfs func(int)dfs = func(idx int) {res = append(res, append([]int{}, path...))for i := idx; i < len(nums); i++ {path = append(path, nums[i])dfs(i + 1)path = path[:len(path)-1]}}dfs(0)return res}
1一进 dfs 就拷贝 path。dfs(0) 时 path 为空,空集在这里收下。
2从 idx 起选下一个数,dfs(i+1) 保证只往前选,[1,2] 不会和 [2,1] 各出现一次。
3递归返回后截掉 path 末尾,下一轮 i 才能站在同一条旧路径上。
4append([]int{}, path...) 是新切片。直接 append(res, path) 会让旧答案跟着 path 一起被改掉。
总结
每个递归入口先拷贝当前路径,再从 idx 往后选。主例 8 个子集,空集在第一次入口。
- 入口时 path 已经是一个子集,不必等到叶子。
- 不拷贝的话,res 里的旧切片会和 path 共用底层数组。
- i+1 向前选,避免同一集合因顺序不同被生成两次。