1、符串全排列问题
打印一个字符串的全排列(比如abc的排列包括:abc,cab,cba,,,,),剑指offer38
又或者,给一串数字,求这串数字的全排列,比如:leetcode46
思路1、首先可以分成两种情况:一种是里面没有重复的字符,一种是有重复的字符(只需要加一个hashset,每次查看需要交换的在set里面是不是已经存在)。整体的结构是:交换其中某两位,然后继续递归后面的,然后在交换回来(恢复交换的 元素),准备迎接下一轮。
过程就如下图所示:
public ArrayList<String> res;
public String[] permutation(String s) {
if(s==null||s.length()<1)
return null;
res=new ArrayList<>();
char[] chas=s.toCharArray();
process(chas,0);
return res.toArray(new String[res.size()]);
}
public void process(char[] chas,int index){
if(index==chas.length)
res.add(String.valueOf(chas));
HashSet<Character> set=new HashSet<>();//防止重复
for(int i=index;i<chas.length;i++){
if(!set.contains(chas[i])){
set.add(chas[i]);
swap(chas,index,i);
process(chas,index+1);
swap(chas,index,i);
}
}
}
public void swap(char[] chas,int i,int j){
char tmp=chas[i];
chas[i]=chas[j];
chas[j]=tmp;
}
思路2、用举的第二个例子来说明。这里不使用上面的交换来实现,而是将每种可能性组成一个链表(LinkedList),每次更新链表的时候查询链表里面存不存在这个元素,如果存在了直接跳过,示意图如下:
public List<List<Integer>> permute(int[] nums) {
if(nums==null||nums.length<1)
return null;
List<List<Integer>> res=new LinkedList<>();//存放结果
backtrack(nums,res,new LinkedList<Integer>());
return res;
}
public void backtrack(int[] nums,List<List<Integer>> res,LinkedList<Integer> tmp ){
if(tmp.size()==nums.length)
res.add(new LinkedList(tmp));
for(int i=0;i<nums.length;i++){
if(tmp.contains(nums[i]))
continue;
tmp.add(nums[i]);
backtrack(nums,res,tmp);
tmp.removeLast();
}
}
2、子集问题
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
leetcode78
思路1:首先也是类似dfs递归的方法,每个位置都可以选择要,或者不要,但是需要注意的一个点是因为我们重复利用了同一个List,因此我们每次add之后别忘记remove。
public List<List<Integer>> subsets1(int[] nums) {
if(nums==null||nums.length<1){
return new ArrayList<>();
}
res=new ArrayList<>();
process(nums,0,new ArrayList<Integer>());
return res;
}
public void process(int[] arr, int index,List<Integer> tmp){
if(index==arr.length)
{
res.add( new ArrayList<>(tmp));
return ;
}
//这里选择了重复利用tmp,没有复制一个新的放入递归里面
process(arr,index+1,tmp);
tmp.add(arr[index]);
process(arr,index+1,tmp);
tmp.remove(tmp.size()-1);//因此最后需要删除当前元素
}
思路2:使用回溯算法,模板如下:
注:这题的数组的长度没有限制,因此每次递归的一开始可以直接把当前结果放入res。不需要等到数的叶子节点。如果像上面一题长度有限制,也就是必须到达叶子节点,那么就不能这样。
public List<List<Integer>> res;
public List<List<Integer>> subsets(int[] nums) {
if(nums==null||nums.length<1){
return new ArrayList<>();
}
res=new ArrayList<>();
backtrack(0,nums,new ArrayList<Integer>());
return res;
}
private void backtrack(int i, int[] nums, ArrayList<Integer> tmp) {
res.add(new ArrayList<>(tmp));//直接返回
for (int j = i; j < nums.length; j++) {
tmp.add(nums[j]);//需要这一位的数据
backtrack( j + 1,nums, tmp);
tmp.remove(tmp.size() - 1);//不需要这一位,直接恢复,等待下一个候选人
}
}
使用回溯算法的示意图:
类似的问题:打印字符串的全部子序列
打印一个字符串的全部子序列,包括空字符串(abc的子序列包括a,ab,abc,bc,,,,)
思路1:使用树的思想,每一位都可以选择要或者不要,因为使用的是String,每次添加字符都会自动新生成一个String对象,不是共用一个对象,所以不需要每次使用完之后删除。
思路2:使用上面的回溯模型,因为没有规定相同的长度,所以不必等到叶子节点才返回。每一轮直接就可以返回。
3、组合问题
输入两个数字 n, k,算法输出 [1…n] 中 k 个数字的所有组合。比如输入 n = 4, k = 2,输出如下结果,顺序无所谓,但是不能包含重复(按照组合的定义,[1,2] 和 [2,1] 也算重复):leetcode77
使用回溯算法的时候,示意图如下:
public List<List<Integer>> combine(int n, int k) {
if(n<1||k<1)
return new ArrayList<>();
List<List<Integer>> res=new ArrayList<>();
backtrack(n,1,k,res,new ArrayList<Integer>());
return res;
}
public void backtrack(int n,int index,int k,List<List<Integer>> res,List<Integer> tmp){
if(tmp.size()==k)
res.add(new ArrayList<>(tmp));
for(int i=index;i<=n;i++){
tmp.add(i);
backtrack(n,i+1,k,res,tmp);
tmp.remove(tmp.size()-1);
}
}
总结
这三类题目非常相似,但是又略有不同,只要记住了回溯的那个框架,其实都能写出来,需要注意的是两个问题:
第一个:这个要求的子序列是不是长度固定的,如果不是,那么在回溯里面直接就可以加入结果集,而不需要等到长度一定(到达叶子节点)时才能返回结果。
第二个:如果其中暂存每种结果的集合是公用的,每次add之后必须remove。
最重要的还是脑海里面有一张上面的二叉树的那张图,有了这个画面就好写了。