LeetCode子集问题回溯实现中为何必须使用展开语法?
你在求解LeetCode子集生成题目时,基于回溯思路实现了如下可正常运行的代码:
function subset(set) { const result = []; helper(set, 0, [], result); return result; } function helper(set, i, slate, result) { if(i === set.length) { result.push([...slate]); return result; } // exclude i helper(set, i+1, slate, result); // include i slate.push(set[i]); helper(set, i+1, slate, result); slate.pop(); } subset([1,2,3])
你对代码中result.push([...slate])这行的逻辑存在疑问,核心困惑点包括:
- 为什么把代码改成
result.push(slate)无法正常运行?你猜测是JavaScript数组按引用存储,后续slate.pop()会同步修改已经推入result的内容,这个判断是否正确? - 为什么把代码改成
result.push(...slate)也无法正常运行?你观察到这种写法是把单个元素逐个推入,而非把包含这些元素的数组作为整体推入,背后的原因是什么?
具体原理解释
1. result.push(slate) 失效的本质
你的理解完全正确。
JavaScript中的数组属于引用类型,当你直接把slate作为参数传给push方法时,result数组中存储的并不是slate的独立值拷贝,而是和slate指向同一块内存地址的引用。后续回溯流程中执行slate.push()、slate.pop()修改slate内容时,所有通过该引用关联的数组(包括已经存入result的条目)都会被同步修改。
等整个回溯流程执行完毕,所有曾经被push进result的引用,最终都会指向回溯路径走完后被清空的空slate,最终result中存储的全是空数组,完全无法得到预期的子集结果。
2. result.push(...slate) 失效的本质
这个写法的问题核心是展开运算符的作用场景差异:
- 当
...slate作为push方法的独立参数传入时,展开运算符会把slate内的所有元素拆解开,作为push的多个独立入参执行追加操作。比如此时slate的值为[1,3],那么result.push(...slate)等价于result.push(1, 3),最终效果是把1、3两个数字直接追加到result数组的末尾,而非把[1,3]作为一个子数组存入result。 - 你观察到的“这种写法是将单个元素逐个推入,而非将包含这些元素的数组作为整体推入”是完全准确的。这种写法最终会把所有回溯路径上的元素全部平铺成一个一维数组,完全不符合“二维数组存储所有子集”的预期结构。
3. 正确写法result.push([...slate])的原理
这里的方括号包裹...slate,本质是借助数组字面量和展开运算符,快速生成一份slate的浅拷贝:
展开运算符会把slate中的所有元素拆出来,放到一个全新的数组字面量中,最终生成的新数组和原slate位于完全独立的内存空间。把这份独立拷贝push进result之后,后续回溯过程中对原slate做的push、pop修改操作,都不会影响已经存入result的拷贝内容,最终就能正确留存每一条回溯路径生成的子集结果。
你可以通过以下对比直观感受三种写法的差异(假设当前slate值为[1,3],初始result为空数组):
result.push(slate):result变为[[1,3]],内部数组与原slate共享引用,后续slate修改会同步变动result.push(...slate):result变为[1,3],所有元素被平铺追加到result中,没有形成嵌套的子数组结构result.push([...slate]):result变为[[1,3]],内部是独立的数组拷贝,后续slate修改不影响该条目
内容的提问来源于stack exchange,提问作者Heymanyoulookkindacool

