回溯算法:List 还是 ArrayList?一个深拷贝引发的思考

在学习和使用回溯算法解决问题时,我们经常会遇到需要维护一个结果列表,例如所有可能的子集、组合或排列。 这个结果列表通常是一个 List<List<Integer>>,其中内部的 List<Integer> 代表一个具体的解。

然而,在构建这些内部的 List<Integer> 时,我们应该使用 List 接口还是 ArrayList 类呢? 这个问题看似简单,但背后隐藏着一个关于深拷贝和浅拷贝的重要概念,它直接影响到回溯算法的正确性。

问题重现:回溯算法中的陷阱

让我们考虑一个简单的例子:使用回溯算法找到一个数组的所有子集。 一个常见的实现方式如下:

复制代码
import java.util.ArrayList;
import java.util.List;

public class Subsets {

    public static List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), res);
        return res;
    }

    private static void backtrack(int[] nums, int index, List<Integer> currentSubset, List<List<Integer>> res) {
        // 基本情况:将当前子集添加到结果列表
        res.add(currentSubset); // 潜在的问题!

        // 递归探索
        for (int i = index; i < nums.length; i++) {
            currentSubset.add(nums[i]); // 选择
            backtrack(nums, i + 1, currentSubset, res);
            currentSubset.remove(currentSubset.size() - 1); // 撤销选择 (回溯)
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        List<List<Integer>> allSubsets = subsets(nums);
        System.out.println(allSubsets);
    }
}

展开

这段代码看起来很合理,但如果你运行它,你会发现结果是错误的! 例如,对于输入 [1, 2, 3],你可能会得到类似 [[], [], [], [], [], [], [], []] 的结果,而不是预期的 [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

问题出在哪里?

问题在于这行代码:

复制代码
res.add(currentSubset);

这里,我们直接将 currentSubset 添加到 res 中,而没有进行任何拷贝。 这意味着 res 中的所有元素都指向同一个 currentSubset 对象。 当我们在回溯过程中修改 currentSubset 时,res 中的所有元素都会受到影响,最终导致错误的结果。

深拷贝的必要性

为了解决这个问题,我们需要对 currentSubset 进行深拷贝,然后再将其添加到 res 中。 深拷贝会创建一个新的 ArrayList 对象,并将 currentSubset 中的所有元素复制到这个新的 ArrayList 中。 这样,res 中的每个元素都将指向一个独立的列表,而对 currentSubset 的修改不会影响到 res 中的其他元素。

正确的代码如下:

复制代码
import java.util.ArrayList;
import java.util.List;

public class Subsets {

    public static List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), res);
        return res;
    }

    private static void backtrack(int[] nums, int index, List<Integer> currentSubset, List<List<Integer>> res) {
        // 基本情况:将当前子集添加到结果列表
        res.add(new ArrayList<>(currentSubset)); // 深拷贝!

        // 递归探索
        for (int i = index; i < nums.length; i++) {
            currentSubset.add(nums[i]); // 选择
            backtrack(nums, i + 1, currentSubset, res);
            currentSubset.remove(currentSubset.size() - 1); // 撤销选择 (回溯)
        }
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 3};
        List<List<Integer>> allSubsets = subsets(nums);
        System.out.println(allSubsets);
    }
}

展开

现在,res.add(new ArrayList<>(currentSubset)) 创建了一个 currentSubset 的副本,并将这个副本添加到 res 中。 这样,res 中的每个元素都指向一个独立的列表,结果就是正确的。

相关推荐
uhakadotcom15 分钟前
BentoML远程代码执行漏洞(CVE-2025-27520)详解与防护指南
后端·算法·面试
_x_w17 分钟前
【16】数据结构之基于树的排序算法篇章
开发语言·数据结构·python·算法·链表·排序算法
西门吹雪分身32 分钟前
Redis之RedLock算法以及底层原理
数据库·redis·算法
一路向北he1 小时前
杰理10k3950温度测量
java·数据结构·算法
描绘一抹色1 小时前
力扣-hot100(最长连续序列 - Hash)
数据结构·算法·leetcode
黄昏ivi1 小时前
事件触发控制与响应驱动控制的定义、种类及区别
人工智能·分布式·学习·算法·机器学习
温文尔雅透你娘2 小时前
摄像头在自动驾驶中的核心应用:感知算法与技术方案深度解析
人工智能·算法·计算机视觉·目标跟踪·自动驾驶
Vacant Seat2 小时前
贪心算法-跳跃游戏
算法·游戏·贪心算法
是小满满满满吗2 小时前
基础贪心算法集合2(10题)
算法·贪心算法
小林熬夜学编程2 小时前
【高阶数据结构】第三弹---图的存储与遍历详解:邻接表构建与邻接矩阵的BFS/DFS实现
c语言·数据结构·c++·算法·深度优先·图论·宽度优先