Java - 为啥必须为每个回溯基本案例创建一个新实例?
Posted
技术标签:
【中文标题】Java - 为啥必须为每个回溯基本案例创建一个新实例?【英文标题】:Java - Why do you have to create a new instance for each backtracking base case?Java - 为什么必须为每个回溯基本案例创建一个新实例? 【发布时间】:2018-07-14 23:19:43 【问题描述】:一个经典的回溯示例是生成整数列表的所有(不一定是不同的)排列。
例如,这是我编写的一个函数:
List<List<Integer>> perm_set = new ArrayList<>(); // store permutations
private void permute(int[] nums, List<Integer> perm, boolean[] used)
if (perm.size() == nums.length)
// System.out.println(perm);
perm_set.add(new ArrayList<>(perm)); // why is it necessary to add new?
return;
for (int i = 0; i < nums.length; i++)
if (used[i])
continue;
used[i] = true;
perm.add(nums[i]);
permute_simple(nums, perm, used);
used[i] = false;
perm.remove(perm.size() - 1);
上述功能有效,但我不明白为什么每次要添加排列时都需要new ArrayList<>(perm)
,即在每次回溯搜索完成时。我添加了一个打印语句来尝试查看发生了什么,果然排列打印得很好。但是如果我只使用perm_set.add(perm)
,我最终会得到一个空白列表。
那么,如果每次搜索结束时的当前排列状态是正确的,为什么我必须为每个案例创建一个新副本?
请注意,该示例是任意的。这个问题并不特定于我提供的代码或特定于排列。它更广泛地适用于典型的 DFS 问题。我想了解为什么通常不能简单地直接存储最终结果,以及为什么需要 new
关键字。
谢谢
【问题讨论】:
什么是perm_set
?这是发布的代码中唯一一次使用它。另外,nums
是什么?这个问题缺乏很多细节和相关背景。
是的,它缺少细节,因为示例并不重要。该示例仅用于演示有关回溯的一般概念问题。您询问的所有变量都不会改变或协助基本问题。我在问为什么每次搜索结束时都需要 new
关键字。排列示例是任意的,可能是任何其他典型的 DFS 问题。现在由于您不必要的反对,我不太可能得到有助于我了解这种行为的回应。
因为如果你只是添加perm
,它仍然可以在外部进行修改。通过创建新列表,您可以确保一旦添加到 perm_set
数组中,对 perm
变量的修改不会影响已添加到 perm_set
的列表
所以对perm
的进一步修改仍然会影响列表中的一些perm
?那是因为列表是可变的吗?这是有道理的,那么当问题处理例如字符串时,为什么不需要新实例。
我不相信代码如图所示会真正起作用。首先,缺少perm_simple
的定义。
【参考方案1】:
考虑以下几点:
这将完全按预期工作。
@Test
public void testPass()
ArrayList<String> listA = new ArrayList<String>();
listA.add("a");
ArrayList<String> listB = new ArrayList<String>(listA);
Assert.assertTrue(listB.get(0).equals("a"));
这将失败,因为如果在复制后修改了原始列表。
@Test
public void testFail()
ArrayList<String> listA = new ArrayList<String>();
ArrayList<String> listB = new ArrayList<String>(listA);
// A, modified after the copy has been made
listA.add("a");
Assert.assertTrue(listB.get(0).equals("a"));
// This will throw and IndexOutOfBoundsException, as listB is empty.
这将失败,因为listB
为空。在修改之前,它已获得 listA 的 副本。
【讨论】:
啊,明白了。所以就像没有创建new
实例一样,它们都指向同一个对象,因此所有修改都将应用于每个指针的引用。
正确,使用旧数组创建一个新数组,本质上将创建该数组的副本,因此以后的修改不会影响原始集合。这不仅仅是new
,而是您将原点传递给constructor。【参考方案2】:
(这是一个更一般的答案的尝试,认识到该问题通常是关于 DFS 的。)
那么,如果每次搜索结束时当前排列的状态是正确的,为什么我必须为每个案例创建一个
perm
的新副本?
简短回答:这是必要的,因为您要更改perm
。
更长的答案:您的List<List<Integer>
正在记录正确解决方案的状态快照。如果您没有复制状态(即拍摄快照),您最终会得到包含对同一 ArrayList<Integer>
对象的 N 个引用的 perm_set
;即最后一个排列的 N 个副本。
请注意,在您的示例代码中,策略是在每次递归调用之前修改状态(perm
和 used
),然后在返回时撤消修改。另一种策略是在递归之前对状态进行快照,然后丢弃状态。如果您要尝试并行化算法,或者撤消的成本或复杂性太高,那么这种方法会更好。
【讨论】:
以上是关于Java - 为啥必须为每个回溯基本案例创建一个新实例?的主要内容,如果未能解决你的问题,请参考以下文章
在“分叉”一个进程时,为啥 Linux 内核会为每个新创建的进程复制内核页表的内容?
继承java.lang.Thread类并重写run方法为啥不可以创建一个新线程呢!为啥?
在语言建模中,为啥我必须在每个新的训练时期之前初始化隐藏权重? (火炬)