11 暴力递归
0、暴力递归本质:
1,把问题转化为规模缩小了的同类问题的子问题
2,有明确的不需要继续进行递归的条件(base case)
3,有当得到了子问题的结果之后的决策过程
4,不记录每一个子问题的解
ps:
递归函数(函数意义就是问题的解)先用后算,函数当成黑盒,可以直接当作该情况下所有的解!
最精髓的地方在于,函数先用后算
难点:
对暴力递归函数的理解,怎么去定义它,它怎么去实现你想的目的
解决:
如上图所示,我们需要关心的主要是以下三点:
- 整个递归的终止条件。
- 一级递归需要做什么?
- 应该返回给上一级的返回值是什么?
因此,也就有了我们解树形递归题的三部曲:
- 找整个递归的终止条件:递归应该在什么时候结束?
- 找返回值:应该给上一级返回什么信息?
- 本级递归应该做什么:在这一级递归中,应该完成什么任务?
为什么有些处理步骤在递归前,有些又在递归后
处理在递归后: 因为当前层需要子递归返回信息才能处理当前当前层信息!
如经典的二叉树的递归套路,如求树深
题目很简单,求二叉树的最大深度,那么直接套递归解题三部曲模版:
1、找终止条件。 什么情况下递归结束?当然是树为空的时候,此时树的深度为0,递归就结束了。
2、找返回值。 应该返回什么?题目求的是树的最大深度,我们需要从每一级得到的信息自然是当前这一级对应的树的最大深度,因此我们的返回值应该是当前树的最大深度,这一步可以结合第三步来看。
3、本级递归应该做什么。 首先,还是强调要走出之前的思维误区,递归后我们眼里的树一定是这个样子的,看下图。此时就三个节点:root、root.left、root.right,其中根据第二步,root.left和root.right分别记录的是root的左右子树的最大深度。那么本级递归应该做什么就很明确了,自然就是在root的左右子树中选择较大的一个,再加上1就是以root为根的子树的最大深度了,然后再返回这个深度即可。
处理在递归后:子递归层需要给当前层提供递归信息,所以在递归前就处理好了边界
如快排问题:见下面案例
//分析: 1、本层递归终止条件 L==R
//2、需要返回给上次什么值 这道题很神奇,
//3、该做什么:当前层partition处理左右边界,方便下一级递归处理
public void process(int[] arr, int L, int R){
if (L<R){
return;
}
if (L == R){
return ;
}
int[] wide= partition(arr,L,R);
process(arr,L,wide[0]);
process(arr,wide[1],R);
}
1、暴力的解法:
1、小样本尝试,画尝试图再改出递归树
2、设计递归函数(参数)
难点: 在于设计f函数,让f函数能实现缩小问题规模。
题目1:汉诺塔
尝试: f(a,c,b,n)
f()意义 将a上1-n个盘子挪动到c上,b为中间盘
f(a,c,b,n){
f(a,b,c, n-1)
n a--> c;
f(b,c,a,n-1);
}
尝试过程1,之后再优化! 就是3大步 1、 1-n-1大坨从A-》B 2、 n从A-》C, 3、1-n-1从B-》C
f(from , to, next, n){
//base case
if(n == 1){
sout("第" +n+ "从"+ form +"移动到"+ to);
return;
}
f(from,next,to,n-1);//别考虑递归过程 直接就是3大步
sout("第" +n+ "从"+ form +"移动到"+ to);
f(next,to,from,n-1);
return;
}
前言:
举例说明 a,b,c,d字符串,想好尝试模型
子序列:4个字符的额取舍问题,一共2^4种情况 ,关键是要不要,二叉树
子串: 连续字符如a ab abc abcd 一共(n+1)n/2种情况
排列: Ann ,全排列问题关键在于选择谁,需要用for尝试后续情况
题目2:打印一个字符串的全部子序列
尝试图: 二叉树结构的递归树
改出来的递归图
递归尝试:
本质:DFS
//参数只要自变量
list<string> ans;
char[] str ;
void f(int index, string path){
//base case
if(index == str.length){
ans.add(path);
}
//other
/*2种情况
1、不要
2、要
*/
f( index+1, path); //不要
string path = path + String.valueOf(str[index]); //局部变量在方法栈调用结束时回收,不存在回复现场
f( index+1, path); //要
}
题目3:打印一个字符串的全部子序列,要求不要出现重复字面值的子序列
method 、 在题目2中使用HashSet作为答案的容器 ,但是任然是全遍历,
题目4:打印一个字符串的全部排列
如:[a,b,c]
method1、需要申请额外的空间结构
尝试树
递归方程:
//方法中的参数在尝试过程能找到
public static void process3(boolean[] is ,char[] chars , int index , String path , ArrayList<String> ans){
if(index == chars.length){
ans.add(path);
return;
}
//这里的j = 0 ,原因是,每次尝试过程中需要尝试所有的字母
for (int j = 0 ; j<chars.length ; j++){
//没有用过
if ( !is[j] ){
String next =path + chars[j]; //路径不共享
is[j] = true;
process3(is,chars,index+1,next,ans);
is[j] = false; //共享 需要恢复现场
}
//使用过 直接跳过
}
}
ps 注意: 方法中的信息,需要共享数据要恢复现场, 私有:局部变量不管!
method2、不需要申请额外的空间结构
尝试树:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvjXmIFh-1616248116987)(C:\Users\huihui520\AppData\Roaming\Typora\typora-user-images\image-20201215094600427.png)]
递归方程
/* 1、 list -> string[]
res.toArray(new String[length]);
2、char[]->String
String.valueOf(str)
不要用toString 在没有重写时,返回@+hash码
3、 暴力递归在记录答案路劲不能直接添加,需要new出来
4、利用hashset完成遍历过程中剪枝去重
*/
//注意没有申请额外的空间
f(char[] str,int index,ArrayList<String> ans){
//base case
if(index == str.length){
ans.add(String.valueof(str);
}
//other
//尝试str中index后字符串中的字母,前面的就不需要动了
for( int j =index ; j<str.length ; j++){
swap(str , index ,j)
f(str , index+1, ans); //递归过程深度优先 可以整体
swap(str , index, ans); //恢复现场
}
}
题目5:打印一个字符串的全部排列,要求不要出现重复的排列
method1 、 在题目4中使用hashset作为答案的容器 ,但是任然是全遍历
method2、 在遍历中加入终止条件(剪枝)
尝试树:
递归函数:
//分支限界 剪枝方法
//再全排列的基础上使用了set,让i位置上出现的字符记录,重复的字符就不用去跑了!!
public static void process2(char[] str, int i, ArrayList<String> res) {
if (i == str.length) {
res.add(String.valueOf(str));
}
// 0代表 a 位置有没有出现过
//这个Boolean和使用hashset一样!!本质:判断有无出现过
boolean[] visit = new boolean[26]; // visit[0 1 .. 25]
//每次大循环都定好i位置的所有情况
for (int j = i; j < str.length; j++) {
//str[j] = "a" 下面等同a出现没 就是hash表
if (!visit[str[j] - 'a']) {
visit[str[j] - 'a'] = true;
swap(str, i, j);
process2(str, i + 1, res);
swap(str, i, j);
}
}
}
while 和递归的区别
while 不递归,每次也缩小规格,但是不能不存中间变量
递归可以缩小规模,存储中间变量!
使用范围比较
循环能干的事,递归都能干;递归能干的事,循环不一定能干。如果使用循环并不困难的话,最好使用循环。
递归中用好
边界和比较条件,可以剩下很多事情
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if(dfs(board, words, i, j, 0)) return true;
}
}
return false;
}
boolean dfs(char[][] board, char[] word, int i, int j, int k) {
//边界+比较! 先筛选后比较
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
if(k == word.length - 1) return true;
//打标记
board[i][j] = '\0';
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
//回复现场
board[i][j] = word[k];
return res;
}
}
剑指 Offer 62. 圆圈中最后剩下的数字
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
public class Ti62new {
//多个步骤之间存在影响,可以先不去考虑,每个步骤独立开来实现,最后再互相叠加
//问题: 如何记录每一轮删处的数
//实际上 : f(n,m)是记录最后一个留下来的位置 而不是数
//递归的向 (n,m)时,先将n张牌的第 m-1%n 的位置删除
//此时只剩 (n-1,m) 先不管之前删的是谁,单独的考虑 f(n-1,m)留下的位置是谁 假如是x, 那么之前删除m-1%n 就会来到m的位置
//然后 此时留下的就不是X , 而是 (m%n +x)%n 了 这样就形成了递推 base case f(n = 1) 就为0
//递归函数的好处就是可以 将大规模缩小为同等难度的子问题 要有这种意识
public int lastRemaining(int n, int m) {
if(n == 1 ){
return 0;
}
//其余情况为 向右移动了 m%n的 f(n-1,m)的情况
return ( lastRemaining(n-1 ,m) + m )% n ;
}
}
快排:
结合之前的分析,这道题的处理在递归前!
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// ==区域位置
int[] equalArea = netherlandsFlag(arr, L, R);
//递归 小于 和 大于区域
process1(arr, L, equalArea[0] - 1);
process1(arr, equalArea[1] + 1, R);
}
public static int[] netherlandsFlag(int[] arr, int L, int R) {
//无效数组
if (L > R) {
return new int[] { -1, -1 };
}
//仅一个元素的数组 base case
if (L == R) {
return new int[] { L, R };
}
// 小于区域有边界
int less = L - 1;
//大于区域左边界(包含了arr[R],让该点先不动作为划分值,最后再交换)
int more = R;
// i
int index = L;
//i 和 more装上 停!!
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
//小于划分值 小于区域有边界 和 该数交换,小于区域++ i++
swap(arr, index++, ++less);
} else {
//大于划分值 index 和大于区域左边界的左一位交换, 大于区域左边界--
swap(arr, index, --more);
}
}
// 区域< == >
//L....less less+1 ..more-1 more...R-1 R
//排好队后,最后交换arr[R] 大于区域第一个
swap(arr, more, R);
//less+1 等于区左边界+1 more右边界 -1 这样才不会出现越界
return new int[] { less + 1, more };
}