
在回溯算法中,若直接将待选列表(如 list)添加到结果集而不创建副本,会导致所有结果项最终都指向同一个被反复修改的列表对象,从而产生空列表或重复错误。关键在于理解 Java 中对象引用的本质与深拷贝的必要性。
在回溯算法中,若直接将待选列表(如 `list`)添加到结果集而不创建副本,会导致所有结果项最终都指向同一个被反复修改的列表对象,从而产生空列表或重复错误。关键在于理解 java 中对象引用的本质与深拷贝的必要性。
在解决类似 LeetCode「Combination Sum」这类组合问题时,递归回溯是常用策略。核心难点之一在于:如何安全地保存当前路径状态(即满足条件的组合)到最终结果中? 你提供的代码中,关键修复行是:
result.add(new ArrayList(list)); // ✅ 正确:添加一份独立副本
而非:
result.add(list); // ❌ 错误:仅添加引用,后续修改会影响已存入的结果
为什么 result.add(list) 会失败?
Java 中 List 是引用类型。list 变量始终指向堆内存中的同一个 ArrayList 对象。在递归过程中,你不断执行:
-
list.add(candidates[ind])→ 修改该对象内容 -
list.remove(list.size()-1)→ 再次修改该对象内容
当调用 result.add(list) 时,只是把同一个引用地址存入 result。随着递归深入和回溯,这个被共享的 list 对象会被反复清空、追加、删除——最终所有存入 result 的“组合”,其实都是对这一个空列表的多次引用。
以输入 [2,3,6,7], target = 7 为例:
- 当找到
[2,2,3]并调用result.add(list)时,list引用被存入; - 后续回溯中
list被remove()清空至[],并继续搜索其他分支; - 最终
result中两个元素都指向同一个已变为空的list实例 →[[ ], [ ]]。
正确做法:每次保存独立副本
new ArrayList(list) 构造了一个新列表,其内容与当前 list 相同,但内存地址不同,后续对原 list 的任何操作都不会影响该副本。这是典型的浅拷贝(shallow copy),对 Integer 等不可变类型完全安全。
✅ 推荐写法(清晰、安全):
if (sum == target) {
result.add(new ArrayList(list)); // 创建快照
return;
}
⚠️ 注意事项:
- 不要试图在递归外复用
list并手动clear()—— 回溯逻辑天然依赖单个可变列表,手动清理极易出错; - 若列表内含可变对象(如自定义类),需实现深拷贝逻辑;但本题中元素为
Integer,new ArrayList(list)已足够; - 部分开发者尝试
list.clone(),但ArrayList.clone()返回Object,需强制转换,且语义不如构造函数直观,不推荐。
总结
回溯算法中,“保存当前路径” ≠ “保存路径引用”,而是“保存路径快照”。每一次 result.add(...) 都应是一次独立的对象创建行为。这是理解 Java 引用机制与回溯状态管理的关键分水岭。掌握这一原则,不仅能修复本题,更能避免在排列(Permutation)、子集(Subsets)等同类问题中出现隐晦的引用污染 bug。










