故事的起源
起源于在leetcode上面刷题,刷到第39题Combination Sum。
Combination Sum
这道题是一道经典的回溯问题,一个被票选为最佳答案的解决方案如下:
public class Solution {
public List<List<Integer>> combinationSum(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
if(remain < 0) return;
else if(remain == 0) list.add(new ArrayList<>(tempList));
else{
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i); // not i + 1 because we can reuse same elements
tempList.remove(tempList.size() - 1);
}
}
}
}
当然还是有更加快速的方法的,请提交后参见More Details里面那个14ms的方法。
我的内心活动
因为我是以前用C++,最近才开始用Java的,当做到这一道题目的时候,我不禁心头一震,要是在整个解空间里不停地调用函数backtrack,是不是所有的参数都要被复制好多好多份,放在内存里,当测试用例特别大的时候,会不会造成很高的空间复杂度?
这时我想起了C++的引用传值大法,这样的话就不会在调用函数的时候把那些参数变量复制一份,大家一起公用一份就可以了,那么我自然而然的想到,Java中的引用参数是怎么玩的?
对Java面向对象理解
然而我的疑惑很快就解开了,大家可以看《Head First Java》这本书的55页到58页。Java中的每一个变量,本质上都是一个“遥控器”。一般声明一个变量比如 tv a; ,表示我买了一个新的遥控器a(注意,前面的tv应该理解成遥控器a是专用来遥控电视的,遥控不了空调等其他设备),a = new tv; 表示家里新买了一台电视,然后我们用我们已经有的遥控器来绑定这台电视。
假如有一天遥控器丢了,或者去遥控其他的电视了,那么Java虚拟机会帮助我们回收这台电视。
回到回溯法上面来
那么,显然我对于回溯问题巨大的解空间树的担心是多余的。因为每一次调用函数backtrack,参数其实都只是遥控器而已,遥控器嘛,能占多大的地方呢?表面看上去是一个参数的复制(C++视角),实际上不过是用两个遥控器遥控同一台电视而已(Java视角),电视一直就只占一份内存,遥控器嘛,多几个又怎么样呢?
我想这大概就是这么多人喜欢Java的原因之一吧。)——来自一个Java菜鸟的无病呻吟。