
在回溯算法中,若直接将待选列表(如 list)加入结果集而不创建新副本,会导致所有结果项最终都指向同一个被反复修改的列表对象,从而输出多个空列表或错误组合。
在回溯算法中,若直接将待选列表(如 `list`)加入结果集而不创建新副本,会导致所有结果项最终都指向同一个被反复修改的列表对象,从而输出多个空列表或错误组合。
在解决类似 LeetCode「Combination Sum」这类需要枚举所有可行组合的问题时,一个常见且关键的细节是:必须在找到合法解时对当前路径列表进行深拷贝(shallow copy),而非直接添加引用。
你的原始代码中这行至关重要:
result.add(new ArrayList(list));
它创建了 list 当前状态的一个独立副本,并将其加入 result。而如果替换为:
result.add(list); // ❌ 错误:添加的是引用本身
则所有成功路径都会向 result 中添加同一个 ArrayList 实例的引用。由于回溯过程中持续调用 list.add(...) 和 list.remove(...),这个共享列表的内容会不断被修改——即使某次递归满足 sum == target 并“加入”了该列表,后续的回溯操作仍会清空或篡改它。最终 result 中所有元素都指向那个已被还原为空的 list,因此输出 [[], []]。
✅ 正确做法始终是:在触达递归终止条件(即找到一个有效组合)时,立即固化当前状态:
if (sum == target) {
result.add(new ArrayList(list)); // ✅ 创建快照
return;
}
⚠️ 注意事项:
-
new ArrayList(list)是浅拷贝,但对Integer等不可变类型已完全足够; - 不要试图在递归外维护多个
list实例——那会极大增加空间开销;标准回溯模式正是靠单个可复用列表 + 及时拷贝来平衡时间与空间; - 类似陷阱也出现在
StringBuffer/StringBuilder、自定义对象列表等场景中:只要对象可变且会被后续修改,就必须拷贝。
总结:回溯的本质是「试探—记录—撤销」,而「记录」必须是瞬时快照。忽略拷贝,等于用一张不断擦写的草稿纸去存档所有答案——最后只剩最后一笔的痕迹。










