被一块石头绊倒两次是真的丢人——不写引子不舒服斯基
递归思想
递归的思想中有几个很重要的特性,对于使用递归求解的问题,把握好这几个因素就能把代码写好了。
1. 终止条件。递归的方法有一个最终的终止条件,这个条件满足题目所提出的要求,而且不会无限的循环下去。
2. 子问题。问题中的子问题可以再次递归调用方法求解。这就是分治思想的运用了。在没有满足返回条件之前,每一步和前一步都只是在状态上有不同。
不带重复元素的
题目
给定一个数字列表,返回其所有可能的排列。
注意事项
你可以假设没有重复数字。
样例
给出一个列表[1,2,3],其全排列为:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解题思路
不带重复元素这个题是很早之前做的了。思路也大概就是写一个递归方法,在方法中判断是否已经全排列,是的话集合加一并返回,不是的话就循环。
很简单的递归全排列思路,甚至可以直接点地记下来。
public class Permute {
public static List<List<Integer>> permute(int[] nums){
List<List<Integer>> res = new ArrayList<>();
ArrayList<Integer> list = new ArrayList<>();
per(res,list,nums);
return res;
}
public static void per(List res,List list,int[] nums){
if(list.size() == nums.length){
res.add(new ArrayList(list));
return ;
}
for(int i = 0;i < nums.length; i++){
if(list.contains(nums[i]))
continue;
list.add(nums[i]);
per(res,list,nums);
list.remove(list.size()-1);
}
}
}
带重复元素的
题目
给出一个具有重复数字的列表,找出列表所有不同的排列。
给出列表 [1,2,2],不同的排列有:
[
[1,2,2],
[2,1,2],
[2,2,1]
]
递归解法
和上面的解法基本上一样,只是要多加一个visited数组来记录这个数据在这一次逐渐深入的递归中是否被使用过,递归返回时改回未被使用。因为允许列表元素重复,所以进行一次普通的全排列后,可能会出现两个相同的排列,为了避免出现重复的排列,所以要进行一次contains的判断。
public class PermuteUnique {
public List<List<Integer>> permuteUnique(int[] nums){
List<List<Integer>> ret = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
int[] visited = new int[nums.length];
permute(0,nums.length,ret,temp,nums,visited);
return ret;
}
/**
* 用来做递归的子方法,可以当模版参考
*
* @param k 当前长度
* @param n 每个的长度,nums.length
* @param ret 返回的那个list
* @param temp 每一次递归用的list
* @param nums 初始数组
* @param visited 元素是否被访问过
*/
public void permute(int k,int n,List ret,List temp,int[] nums,int[] visited){
if(k == n && !ret.contains(temp)){
ret.add(new ArrayList(temp));
}
if(k > n){
return;
}
else{
for(int i = 0;i < n;i++){
int pre = nums[i];
if(visited[i] == 0){
temp.add(pre);
visited[i] = 1;
permute(k + 1,n,ret,temp,nums,visited);
visited[i] = 0;
temp.remove(temp.size() - 1);
}
}
}
}
}
这一份代码中还有一些地方是可以优化的,比如那个参数n就是完全多于的东西,按照上一份代码其实这个参数是可以不用的,包括k也是,明明就是一个list.size的事,但是当时写代码的时候本着跑起来再说的想法,没有考虑这些问题,虽然之后发现了,但是本着提醒自己的想法,就把没改的版本贴到了这个博客中。
非递归解法
这个,偷个懒,以后再说。
指针问题
说到指针,按理说学了这么几年已经不应该再在这种事上糊涂了,但是还是犯了错,做之前全排列的那个题时愣是没想通,好在今天做这个题的时候反应过来了,所以借此机会记录一下。
上面的递归代码中,当排序好的List要放入总的集合中时,之前的我是这样操作的。
if(k == n && !ret.contains(temp)){
//stupid
ret.add(temp);
}
用这种add,执行后你会发现结果会大错特错。而应该换用:
if(k == n && !ret.contains(temp)){
ret.add(new ArrayList(temp));
}
对,没错。
使用上面那个错误操作所存进集合中的并不是一个对象,而是指向这个List对象的指针。这就引起了问题:add进集合中的永远是同一个元素,而且,在带重复元素这道题中,需要进行一次是否已经contains的判断,这个会一直为true,让你甚至连重复元素都加不进去。写代码验证一下这个想法:
List<List<Integer>> ret = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
temp.add(5);
ret.add(temp);
System.out.println(ret.contains(temp));
temp.add(7);
System.out.println(ret.contains(temp));
ret.add(temp);
Iterator it = ret.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
执行结果为:
true
true
[5, 7]
[5, 7]
证明确实如此。所以每次进行add操作时,要new一个新的对象用来存放已经排好序的部分。
整体感想
多看,多写,多总结。