递归与回溯总结
递归
在函数中调用自身的方法称为递归。
递归三部曲:
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
回溯
可以理解成多分支的递归,主要是递归+局部暴力枚举来实现。回溯有剪枝功能,去掉那些不必要的递归,从而提高执行效率。
主要分为组合、分割、子集、排列、棋盘等问题。
回溯三部曲:
- 回溯函数模板返回值以及参数
回溯算法中函数返回值一般为void。因为回溯算法需要的参数不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。
- 回溯函数终止条件
什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。
- 回溯搜索的遍历过程
回溯法一般是在集合中递归搜索(for循环,一个节点有多少个孩子,就执行多少次),集合的大小构成了树的宽度,递归的深度构成的树的深度。
// 模板代码
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
startIndex 记录下一层递归,搜索的起始位置,防止出现重复的组合。
对于组合问题,什么时候需要startIndex呢?
- 如果是一个集合来求组合的话,就需要startIndex
- 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
for循环每次从startIndex开始遍历,然后用path保存取到的节点i。
可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
题目中的无限制重复被选取,要注意出现0的情况。看到下面提示:1 <= candidates[i] <= 200,因此本题可以不用再考虑了。
因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回
在求和问题中,排序之后加剪枝是常见的套路!
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(candidates);
backtracking(ans, new ArrayList<>(),candidates,target,0);
return ans;
}
public void backtracking(List<List<Integer>> ans, List<Integer> path, int[] candidates, int target, int idx){
if(target == 0){
ans.add(new ArrayList<>(path));
return;
}
if(target<0) return;
for(int i=idx; i<candidates.length && candidates[i]<=target; i++){
path.add(candidates[i]);
// 使用 i 作为下一个递归的起始索引,这样可以在同一层中多次选择相同的元素。
backtracking(ans,path,candidates,target-candidates[i],i);
path.remove(path.size()-1);
}
}
}
17.电话号码的字母组合
class Solution {
List<String> ans = new ArrayList<>();
StringBuilder temp = new StringBuilder();
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length()==0){
return ans;
}
String[] map = {"","", "abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
backtracking(digits,map,0);
return ans;
}
public void backtracking(String digits, String[] map, int num){
if(num == digits.length()){
ans.add(temp.toString());
return;
}
String str = map[digits.charAt(num)-'0'];
for(int i=0; i<str.length(); i++){
temp.append(str.charAt(i));
backtracking(digits,map,num+1);
temp.deleteCharAt(temp.length() - 1);
// temp.delete(temp.length()-1,temp.length());
}
}
}
class Solution {
// 枚举填左括号还是右括号
public List<String> generateParenthesis(int n) {
List<String> ans = new ArrayList<>();
char[] path = new char[n*2];
backtracking(ans, path, 0, 0,n);
return ans;
}
public void backtracking(List<String> ans, char[] path, int i, int leftCount,int n){
if(i==path.length){
ans.add(new String(path));
return;
}
// 如果左括号个数小于n;可以选左括号
if(leftCount<n){
path[i]='(';
backtracking(ans,path,i+1,leftCount+1,n);
}
// 如果右括号个数i-leftCount小于左括号;可以选右括号
if(i-leftCount<leftCount){
path[i]=')';
backtracking(ans,path,i+1,leftCount,n);
}
}
}
分割问题
131.分割回文串
首先用一个 dp 表来存储s[i,j]是否为回文字符串.
然后从i开始枚举每个j,使得s[i,j]是回文串,然后加入字符串列表,再从j+1开始递归。
public List<List<String>> ret = new ArrayList<List<String>>();
List<String> ans = new ArrayList<String>();
public static boolean[][] dp;
public static int n;
public List<List<String>> partition(String s) {
n=s.length();
dp = new boolean[n][n];
for(int i=0; i<n; i++){
Arrays.fill(dp[i],true);
}
for(int i=n-1; i>=0; i--){
for(int j=i+1; j<n; j++){
dp[i][j] = (s.charAt(i)==s.charAt(j)) && dp[i+1][j-1];
}
}
dfs(s,0);
return ret;
}
public void dfs(String s,int i){
if(i==n){
ret.add(new ArrayList<String>(ans));
return;
}
for(int j=i; j<n; j++){
if(dp[i][j]){
ans.add(s.substring(i,j+1));
dfs(s,j+1);
ans.remove(ans.size()-1);
}
}
}
子集问题
90. 子集 II
这道题其实就是求不同的组合,解集中的子集不重复嘛。
排列与组合的区别:排列可以重复,看的是顺序。组合不能重复。
因为组合不区分顺序,因此可以先排序,从第一组相同元素中选择有n+1个,再加上第二组相同元素的选择以及后续的选择。这个操作中从2^n次展开变成了n+1次,涉及到剪枝的操作。
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
//新建了一个数组,表示路径,还有个size参数来进行路径擦除
f(nums,new int[nums.length],0,0,res);
return res;
}
public void f(int[] nums, int[] path, int i, int size, List<List<Integer>> res){
if(i==nums.length){
ArrayList<Integer> cur = new ArrayList<>();
for(int j=0; j< size; j++){
cur.add(path[j]);
}
res.add(cur);
}else{
//寻找下一组第一个数的位置
int j = i+1;
while(j< nums.length && nums[j]==nums[i]){
j++;
}
// 当前组相同元素中选择0个,因为size没变
f(nums,path,j,size,res);
//尝试增加当前数的个数
for(; i<j; i++){
path[size++]=nums[i];
f(nums,path,j,size,res);
}
}
}
- 时间复杂度:O(2^n * n)。
排列问题
46. 全排列
注意:我们这里执行的是带路径的递归。最后必须执行交换,清理现场,是因为我们的操作都是基于原状态进行操作的,否则会导致结果重复。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
f(nums, 0, res);
return res;
}
public void f(int[] nums, int i, List<List<Integer>> res){
if(i==nums.length){
List<Integer> cur = new ArrayList<>();
for(int num : nums){
cur.add(num);
}
res.add(cur);
}else{
for(int j=i; j<nums.length; j++){
swap(i,j,nums);
f(nums, i+1, res);
swap(i,j,nums);//必须换回来,否则出错
}
}
}
public void swap(int i, int j,int[] nums){
int temp = nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
- 时间复杂度:O(n! * n)。因为结果有n!个排列数,每个排列数的长度n。
46. 全排列Ⅱ
本题与上一题的区别在于,本题给的数组中可能有重复的数字,因此代码只需要做一点小变动。在f 函数的 j 遍历中需要加一个判断,即 j 之前未出现过才能进行 i 与 j 的交换。这里需要用HashSet,当不存在nums[j]时,进行交换,并把nums[j]放进set.
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res =new ArrayList<>();
f(nums, 0, res);
return res;
}
public void f(int[] nums, int i, List<List<Integer>> res){
if(i==nums.length){
List<Integer> cur =new ArrayList<>();
for(int num : nums){
cur.add(num);
}
res.add(cur);
}else{
Set<Integer> set = new HashSet<>();
for(int j =i; j<nums.length; j++){
if(!set.contains(nums[j])){
set.add(nums[j]);
swap(i,j,nums);
f(nums,i+1,res);
swap(i,j,nums);
}
}
}
}
public void swap(int i, int j, int[] nums){
int temp = nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
- 时间复杂度:O(n! * n)。
棋盘问题
面试题 08.06.汉诺塔问题
思路:
将第一个柱子 from 的盘子移到第三个柱子 to ,需要借助第二个柱子 other 。
public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
f(A.size(), A, C, B);
}
public void f(int i, List<Integer> from, List<Integer> to, List<Integer> other){
if(i==1){
to.add(from.remove(from.size()-1));//移除最上面的盘子,对应的是list的最后一个元素
return;
}
f(i-1, from, other, to);
to.add(from.remove(from.size()-1));
f(i-1, other, to, from);
}
- 时间复杂度:O(2^n),因为需要移动2^n-1次。
OR33 用递归函数和栈操作逆序栈
将bottomOut函数理解成一个黑盒子,其作用是:返回栈底元素,并且将栈上面的元素移下来。
import java.util.*;
public class ReverseStack {
public int[] reverseStackRecursively(int[] stack, int top) {
Stack<Integer> sta = new Stack<>();
for(int i : stack){
sta.push(i);
}
reverse(sta);
for(int i= top-1; i>=0; i--){
stack[i]=sta.pop();
}
return stack;
}
public void reverse(Stack<Integer> sta){
if(sta.isEmpty()){
return;
}
int num = bottomOut(sta);
reverse(sta);
sta.push(num);
}
public int bottomOut(Stack<Integer> sta){
int res = sta.pop();
if(sta.isEmpty()){
return res;
}
int last = bottomOut(sta);
sta.push(res);
return last;
}
}
- 时间复杂度:O(n^2),因为bottomOut函数需要遍历栈中n个元素,而逆序时,需要调用n次bottomOut函数。
NC190 字符串的全部子序列
public String[] generatePermutation (String s) {
// write code here
char[] str = s.toCharArray();
HashSet<String> set =new HashSet<>();
f(str,new char[str.length],0,0,set);
int m = set.size();
String[] res =new String[m];
int i=0;
for(String cur : set){
res[i++]=cur;
}
return res;
}
public void f(char[] str, char[] path, int i, int size, HashSet<String> set){
if(i == str.length){
set.add(String.valueOf(path,0,size));
}else{
path[size]=str[i];
f(str,path,i+1,size+1,set);
//这里是为了路径消除
f(str,path,i+1,size,set);
}
}
- 时间复杂度:O(2^n * n)。
因篇幅问题不能全部显示,请点此查看更多更全内容