暴力递归
暴力递归就是尝试
类似于回溯:可以理解为回溯就是在暴力递归的基础上在每个操作步骤加上标记和取消标记的处理
- 把问题转化为规模缩小了的同类问题的子问题
- 有明确的不需要继续进行递归的条件(base case)
- 有当得到了子问题的结果之后的决策过程
- 不记录每一个子问题的解
最优试法
可变参数结构最简单、可变参数最少的方法
形式越简单、数量越少越容易改成动态规划
汉诺塔问题
1~i-1 的盘从from移动到other
i的盘从from移动到to
1~i-1 的盘从other移动到to
public void hanoi(int n){
if(n>0){
func(n,"左","右","中");
}
}
public void func(int i,String start,String end,String other){
if(i==1){//base case
System.out.println("Move 1 from "+start+" to "+end);
}else{
func(i-1,start,other,end);
System.out.println("Move "+i+"from "+start+"to "+end);
func(i-1,other,end,start);
}
}
打印一个字符串的全部子序列,包括空字符串
每个字符有两种选择:选和不选
利用系统栈保存了原来的数据 相当于在回溯
public void printAllSubSequence(String str){
char[] chs=str.toCharArray();
process(chs,0);
}
public void process(char[] str,int i){
if(i==chs.length){
System.out.println(String.valueOf(chs));
return;
}
process(chs,i+1);//要当前字符的路
char tmp=chs[i];//保存
chs[i]=0;
process(chs,i+1);//不要当前字符的路
chs[i]=tmp;//还原
}
力扣115?
//去除一些看不见的字符(0)
cur=cur.replaceAll("\\p{C}", "");
打印一个字符串的全部排列
打印一个字符串的全部排列,要求不要出现重复的排列
str[i…]范围上,所有的字符都可以在i位置上,后续都去尝试
str[0…i-1]范围上,是之前做的选择
与数学中的组合本质一样
从左往右尝试
public ArrayList<String> Permutation(String str){
ArrayList<String> res=new ArrayList<>();
if(str==null||str.length()==0){
return res;
}
char[] chs=str.toCharArray();
process(chs,0,res);
return res;
}
public void process(char[] chs,int i,ArrayList<String> res){
if(i==chs.length){//base case
res.add(String.valueOf(chs));
return;
}
boolean[] visit=new boolean[26];//全是小写字母
for(int j=i;j<chs.length;i++){
if(!visit[chs[j]-'a']){//去重 也可以process完成后在主函数中进行去重(但不推荐)这里相当于是在剪枝 优化时间复杂度
visit[chs[j]-'a']=true;
swap(chs,i,j);
process(chs,i+1,res);
swap(chs,i,j);//还原
}
}
}
力扣46:全排列
给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。回溯,不用去重
class Solution {
public List<List<Integer>> permute(int[] nums) {
if(nums==null||nums.length==0){
return null;
}
int n=nums.length;
List<List<Integer>> res=new ArrayList<>();
List<Integer> help=new ArrayList<>();//key point
for(int i=0;i<n;i++){
help.add(nums[i]);
}
process(n,res,help,0);
return res;
}
public void process(int n,List<List<Integer>> res,List<Integer> help,int i){
if(i==n){
res.add(new ArrayList(help));//key point ArrayList的构造函数只能传Collection的子类 不能直接传数组
return;
}
for(int j=i;j<n;j++){
Collections.swap(help,i,j);//巧妙应用Collections的方法
process(n,res,help,i+1);
Collections.swap(help,i,j);
}
}
}
拿牌游戏
本质:动态规划
怎么通过试法推到动态规划 后续
两个函数互相调用
public int win(int[] arr){
if(arr==null||arr.length==0){
return 0;
}
return Math.max(f(arr,0,arr.length-1),s(arr,0,arr.length-1));
}
//先手拿
public int f(int[] arr,int i,int j){
if(i==j){//base case
return arr[i];
}
return Math.max(arr[i]+s(arr,i+1,j),arr[j]+s(arr,i,j-1));
}
//后手拿
public int s(int[] arr,int i,int j){
if(i==j){/
return 0;
}
return Math.min(f(arr,i+1,j),f(arr,i,j-1));//这里后手拿的时候相当于是先手拿对方拿过的,但先手会把对后手最不利的方式给到后手
}
给你一个栈,请逆序这个栈,不能申请额外的数据结构,只能使用递归函数
要实现一个函数:弹出栈底元素并返回该值
关键:递归栈能保存信息
public int f(Stack<Integer> stack){
int res=stack.pop();
if(stack.isEmpty()){
return res;
}else{
int last=f(stack);
stack.push(res);
return last;//一层一层往上返回
}
}
public void reverse(Stack<Integer> stack){
if(stack.isEmpty()){
return;
}
int i=f(stack);
reverse(stack);
stack.push(i);
}
数字转化成字符串
从左往右尝试
public int number(String str){
if(str==null||str.length()==0){
return 0;
}
return process(str.toCharArray(),0);
}
//i之前的位置,如何转化已经做过决定
//i...有多少种转化结果
public int process(char[] str,int i){
if(i==str.length){
return 1;//把之前做的所有选择的这种结果返回
}
if(str[i]=='0'){
return 0;//0字符代表之前做的选择有错误 0不代表任何字母且不能以0开头
}
//数字1肯定可以和后面的一位数字作为整体 数字2要分情况谈论
if(str[i]=='1'){
int res=process(str,i+1);//i作为单独的部分,后续有多少种方式
if(i+1<str.length){//注意这里的条件判断
res+=process(str,i+2);//i和i+1作为单独的报复嗯,后续有多少种方式
}
return res;
}
if(str[i]=='2'){
int res=process(str,i+1);
//i和i+1作为单独的部分并且没有超过26,后续有多少种方法
if(i+1<str.length&&(str[i+1]>='0'&&str[i+1]<='6')){
res+=process(str,i+2);
}
return res;
}
//这里有潜台词 3-9的数字只可能做单独选择
return process(str,i+1);
}
背包问题
从左到右 要和不要
public int maxValue(int[] weights,int[] values,int bag){
return process(weights,values,0,0,bag);
}
//i...的货物自由选择所形成的最大价值
public int process(int[] weights,int[] values,int i,int alreadyWeight,int bag){
if(alreadyWeight>bag){
return 0;
}
if(i==weights.length){
return 0;//这里两个0背后的含义不一样
}
return Math.max(process(weights,values,i+1,alreadyWeight,bag),values[i]+process(weights,values,i+1,alreadyWeight+weights[i],bag));//不要和要
}
N皇后问题
返回摆放的方式有几种
public int num(int n){
if(n<1){
return 0;
}
int[] record=new int[n];//record[i]->i行的皇后放在了第几列
return process(0,record,n);
}
//返回的是可以摆放的结果有多少种
public int process(int i,int[] record,int n){
if(i==n){
return 1;//base case 之前的做法是对的 返回一
}
int res=0;
for(int j=0;j<n;j++){
//当前i行的皇后,放在j列,会不会和之前(0...i-1)的皇后共行或共斜线
if(isValid(record,i,j)){
record[i]=j;
res+=process(i+1,record,n);
}
}
return res;
}
public boolean isValid(int[] record,int i,int j){
for(int k=0;k<i;k++){//遍历之前已经做过的选择
if(j==record[k]||Math.abs(record[k]-j)==Math.abs(k-i)){//x
return false;
}
}
return true;
}
N皇后问题有更好的思路和算法,但都是很难的
面试掌握程度只需要到这种程度即可
但是可以在这种思路上做一些优化
优化版本:位运算(在时间上优化程度很高)
每一行做的选择都变成了限制信息
斜线:在列的限制加上之后整体移动
//不要超过32皇后问题
public int num(int n){
if(n<1||n>32){
return 0;
}
int limit=n==32?-1:(1<<n)-1;//例如8皇后问题 它就是左边八个0+右边8个1 -1是32位都是1的状态
return process(limit,0,0,0);//后几位都是0即第一位皇后没有限制
}
//colLim列的限制,1的位置不能放皇后,0的位置可以
//leftDiaLim左斜线的限制,rightDiaLim右斜线的限制
public int process(int limit,int colLim,int leftDiaLin,int rightDiaLim){
if(colLim==limit){//base case 列限制信息跟limit一样的时候就是n个皇后都放置完成之后
return 1;
}
int pos=0;
int mostRightOne=0;
pos=limit&(~(colLim|leftDiaLim|rightDiaLim));//pos值为1的数就是现在可以放的位置,(colLim|leftDiaLim|rightDiaLim)是现在所有不能放的位置
int res=0;
while(pos!=0){
mostRightOne=pos&(~pos+1);//取到最右边为1的数
pos=pos-mostRightOne;
res+=process(limit,colLim|mostRightOne,(leftDiaLim|mostRightOne)<<1,(rightDiaLim|mostRightOne)>>>1);//选取最右边为1的数放置皇后之后,加上下一列的限制条件进行递归
}
return res;
}