递归
1. 调用机制
递归就是方法自己调用自己,每次调用传入不同的变量
public class DiGui {
public static void main(String[] args) {
test(4);
}
//打印问题
public static void test(int n) {
if(n>2) {
test(n-1);
}
System.out.println("n="+ n);
}
结果:
n=2
n=3
n=4
java虚拟机分为栈,堆,代码区/常量 三部分
代码从main主方法开始执行
- 在栈中开辟一个main栈帧1号,在里面调用了test(4),当程序执行到一个方法时,就会开辟一个独立的空间
- 在栈中开辟一个新的空间2号中,其中:n=4, if(n>2) test(3);
- 又会开辟一个空间3号,其中:n=3, if(n>2) test(2);
- 又会开辟一个空间4号,其中:n=2,此时不满足 if(n>2) ,开始往下执行输出语句,n=2; (此处位于栈顶),执行完后,此空间就没有了
- 又会重新回到了 空间3号,执行输出语句,n=3;
- 又会重新回到了 空间2号,执行输出语句,n=4; 之后就退出程序。
- 所以,发现从栈底依次向上开辟空间,先从栈顶依次向下执行,直到栈底。
- 还有每个空间中的n是独立的,互相不影响。
2. 规则:(重要)
- 当执行一个方法时,就会创建一个新的受保护的空间(栈空间)。
- 方法中的局部变量是独立的,不会相互影响。但方法中使用引用类型的变量(比如数组),就会共享该引用类型的变量。
- 递归必须向退出递归的条件逼近,否则就会出现无限递归,然后栈溢出,报错java.lang.StackOverflowError
- 当一个方法执行完毕,或者遇到return ,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或返回,该方法也就执行完毕。
3. 自下而上的递归(递推,数学归纳,动态规划)
- 解决简单情况下的问题推广到复杂的情况(由小到大,逐步递推)
- 若递推次数很明确,用迭代
- 若有封闭形式,(递推公式),可以直接求解。
- 递归和迭代地运算顺序是一样的,
1. 上楼梯
有个小孩上楼梯,楼梯有n阶台阶,小孩一次可以上1阶,2阶,3阶。
请实现一个方法,计算小孩有多少种上楼的方式
为了防止溢出,请将结果Mod 1000000007
给定一个正整数n,返回一个数,代表上楼的方式
package 递归;
/*分析:
* 自下而上的递推:就是先从较为简单的开始,之后再复杂。
* 梯数 走法
* 1 1
* 2 2
* 3 4
* 4 7
* 5 13
* 从梯数是4时,可以使用前三个的结果
* 可以:
* 3+1 1种走法
* 2+2 剩两梯时,由前知,2种走法
* 1+3 剩三梯时,由前知,4种走法
* 总结:
* 梯数为n,f(n)=f(n-1)+f(n-2)+f(n-3)
* 得到递推公式*/
public class Digui {
static final long mod = 1000000007;
public static void main(String[] args) {
System.out.println(recursion1(4));
System.out.println(recursion2(4));
}
//由递归法做(简洁)
public static long recursion1(int n) {
if(n<0) return 0;
if(n==0||n==1) return 1;
if(n==2) return 2;
return recursion1(n-1)%mod + recursion1(n-2)%mod +recursion1(n-3)%mod;
}
//由迭代法做(理解推理的过程)
public static long recursion2(int n) {
if(n<0) return 0;
if(n==0||n==1) return 1;
if(n==2) return 2;
if(n==3) return 4;
long x1 = 1;
long x2 = 2;
long x3 = 4;
for(int i=4;i<=n;i++) {//不断地更新三个变量
long x = x1;
x1 = x2 % mod;
x2 = x3 % mod;
x3 = ((x1 + x2)%mod +x)%mod;
}
return x3;
}
}
2. 机器人走方格
有一个X乘以Y的网络,一个机器人只能走格点且向右或向下走,从左上角走到右下角,请设计一个算法,计算机器人有多少种走法
给定两个正整数,int x, int y ,返回机器人的走法数目,保证x+y 小于等于12
package 递归;
/*分析:
X=Y=1 不动,也算1种走法
X=1 Y=2 1种
X=2 Y=1 1种
X=2 Y=2 2种
X=3 Y=2 3种
X=2 Y=3 3种
X=3 Y=3 6种
分析:
当是X=3 Y=2时,若向下走,就面临一个 X=2 Y=2 由前方知道有2种, 若向右走,就面临一个 X=3 Y=1 有1种, 2+1=3
当是X=2 Y=3时,若向下走,就面临一个 X=1 Y=3 由前方知道有1种, 若向右走,就面临一个 X=2 Y=2 有2种, 1+2=3
当是X=3 Y=3时, 若向下走,就面临一个X=2 Y=3 由前方知道有3种 若向右走,就面临一个 X=3 Y=2 有3种 3+3=6
往右走列数减1,往下走行数减1
递推公式: f(x,y) = f(x,y-1) + f(x-1,y)
*/
public class DiGui2 {
public static void main(String[] args) {
System.out.println(zou1(3, 3));
System.out.println(zou2(3, 3));
}
//递归
public static int zou1(int x, int y) {
if(x==1||y==1) {
return 1;
}
return zou1(x, y-1)+zou1(x-1, y);
}
//迭代(要考虑需要多少变量来不断地更新数据,技巧看递推公式,此处有两个参数,需要二维,用二维数组来保存所有变量)
public static int zou2(int m, int n) {
int[][] state = new int[m+1][n+1];
for(int i=1;i<=n;i++) {//初始化第一行
state[1][i]=1;
}
for(int i=1;i<=m;i++) {//初始化第一列
state[i][1]=1;
}
//从第二行第二列开始填充变化的数值(走法),每个元素等于它的相对应的上一行,左一列的两个数之和
for(int i=2;i<=m;i++) {
for(int j=2;j<=n;j++) {
state[i][j] = state[i][j-1]+state[i-1][j];
}
}
return state[m][n];
}
}
3. 硬币表示某个具体的给定值(较难)
package 递归;
/*已知有1分,5 分,10分, 25分 要凑成n分,共有多少种组合?
*分析:
*n=1,2,3,4 有1种
*n=5,6,7,8,9 有2种
*n=10,11,12,13,14 有4种
*n=15 有6种
*当n=10 可以:
*1x10 1种
*0x10 0x5 1种 1x5 1种 2x5 1种
*
*当n=15 可以:
*0x10 0x5 1种 1x5 1种 2x5 1种 3x5 1种
*1x10 0x5 1种 1x5 1种
*
*发现:
*f(15) = f(0个10,剩下15,用1和5凑)+f(1个10,剩下55,用1和5凑)
*f(25) = f(0个25,剩下25,用1,5,10凑)+f(1个25,剩下0,用1,5,10凑)
*f(50) = f(0个25,剩下50,用1,5,10凑)+f(1个25,剩下25,用1,5,10凑)
*当n=100,就可以有 0x25 1x25 2x25 3x25 4x25
* 写递推公式:
* 1. 参数:调用者只决定最大面值用多少个,剩下的面值,用其他剩余的硬币类去凑, 需要两个参数,要凑的数目 和 凑时可以使用的硬币面值种类
*/
public class YingBi {
int[] coins = {1,5,10,25};
public static void main(String[] args) {
for(int i=1;i<=100;i++) {
System.out.println("总数为"+i+"的组合有"+countWays(i));
}
}
/**
* @param n 要凑的总面额
* @param coins 保存可以选择的面值种类
* cur 是可以选择的面值种类个数,对应数组的下标,从0开始
*coins[cur] 表示当前可以选的最大面值
*关键:在for循环中有递归
*/
public static int count(int n,int[] coins,int cur) {
if(cur == 0) return 1;//出口
int c =0;//组合数
for(int i=0;i*coins[cur]<=n;i++) {//执行此处的循环,就是可以实现选择最大面值的个数
int shengyu = n-i*coins[cur];
c+=count(shengyu, coins,cur-1);//可供选择最大面值减小,同时可供选择种类
}
return c;
}
public static int countWays(int n) {
if(n<=0) return 0;
return count(n, new int[] {1,5,10,25}, 3);
}
}
4. 应用:
1. 迷宫
如下是8行7列的一个迷宫地图,现在要从最左上角数字0走到最右下角数字0这个位置,实现自动查找路线。
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
其中1,表示的是墙,是不能触碰的,0表示的是可以走的路线,通过算法,画出从位置(1,1)到(6,5)路线。
package 递归;
public class MiGong {
public static void main(String[] args) {
// 定义一个二维数组,将地图先画出来,墙的位置用1表示,可走的路用0表示
int[][] map = new int[8][7];
// 地图上下置为1,模拟墙.行不变,列在变
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
// 地图左右置为1,模拟墙.列不变,行在变
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
// 设置挡板
map[3][1] = 1;
map[3][2] = 1;
map[1][2] = 1;
map[2][2] = 1;
// 输出地图
System.out.println("地图原貌:");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");// 此处有一点,就是print是不换行输出,而println是换行输出
}
System.out.println();
}
System.out.println("使用递归回溯给小球找路:");
setWay(map, 1, 1);
System.out.println("地图此时面貌:");
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 7; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
// 使用递归回溯来给小球找路
//从起始位置(1,1)到终点位置(6,5),则说明通路找到。
//约定:当map[i][j]=0,表示该点没有走过,为1表示墙,为2表示通路,可以走;为3表示该位置已经走过,但是走不通
//再定一个走的策略:先往下,右,上,左方向的顺序;不同的策略,就会有不同的路径。
//如果该点走不通,再回溯;而且因为递归都是面向同一个地图数组,即在栈中的是引用变量。
/**
* @param map 表示地图
* @param i 起始位置
* @param j 终点位置
* @return 找到就返回true
*/
public static boolean setWay(int[][] map, int i, int j) {
if (map[6][5] == 2) {// 通路已经找到
return true;
} else {
if (map[i][j] == 0) {// 如果当前这个点还没有走过,
// 按照策略:下,右,上,左方向走
map[i][j] = 2;// 假定该点可以走通的,
if (setWay(map, i + 1, j)) {// 向下走
return true;
} else if (setWay(map, i, j + 1)) {// 向右走
return true;
} else if (setWay(map, i - 1, j)) {// 向上走
return true;
} else if (setWay(map, i, j - 1)) {// 向左走
return true;
} else {// 执行到此处,说明该点面向四个方向都走不通,那就标记一下
map[i][j] = 3;
return false;
}
}
else {// map[i][j]!=0,那么它有可能是1(墙),2(已经走过,不要反复走),3(已经走过,但是死路)
return false;
}
}
/*执行结果:
地图原貌:
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
使用递归回溯给小球找路:
地图此时面貌:
1 1 1 1 1 1 1
1 2 0 0 0 0 1
1 2 2 2 0 0 1
1 1 1 2 0 0 1
1 0 0 2 0 0 1
1 0 0 2 0 0 1
1 0 0 2 2 2 1
1 1 1 1 1 1 1 */
}
}
/*分析:
* 从(1,1)开始,执行map[1][1] = 2,先调用递归 向下走,setWay(map, 2, 1)
* 进入新的方法,setWay(map, 2, 1),执行map[2][1] = 2,再调用递归 向下走,走不通,然后向右走,可以
* 就是此执行过程的每个探测点,先往下,走不通,则向右,走通就不用修改map[i][j] = 2,一直到终点,没有回溯;
* 那回溯怎么体现呢?
* 可以再增添挡板
map[1][2] = 1;
map[2][2] = 1
结果:
地图原貌:
1 1 1 1 1 1 1
1 0 1 0 0 0 1
1 0 1 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
使用递归回溯给小球找路:
地图此时面貌:
1 1 1 1 1 1 1
1 3 1 0 0 0 1
1 3 1 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
*分析:
*先是map[1][1] = 2,往下走,走通,再标记map[2][1] = 2,而此时往下,右,上,左都没有走通,那就map[2][1] = 3;
*就会回溯到(1,1),又向右,上,左;都没走通,map[1][1] = 3*/
分析:
1,定义一个二维数组,将地图先画出来,墙的位置用1表示,可走的路用0表示。
2,自定义一个查找路线 下——>右——>上——>左
3,采用递归算法, 实际上是按照策略中的,经过的每个点都递归实现四个方向的判断,走通就标记为2,走不通,就标记为3,并且回溯。
回溯就是利用递归到达边界条件好返回上一层循环
2. 八皇后问题
在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
思路分析:
- 第一个皇后先放在第一行第一列
- 第一个皇后先放在第二行第一列,判断是否可以,若不符合,继续放在第二列,第三列,依次把所有列都放完,找到一个合适的
- 继续第三个皇后,还是第一列,第二列,第三列,直到第八个皇后也能放在一个不冲突的位置,就找到了一个正确解
- 当找到了一个正确解,在栈退回到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解全部得到。
- 然后再将第一个皇后放在第二列,继续循环执行1,2,3,4的步骤。
说明:
棋盘用一维数组表示,arr[i]={0,4,7,5,2,6,1,3} 对应arr下标,,就表示第几行,即第几个皇后,arr[i]=a ,第i+1个皇后,放在第i+1行,第a+1列
package 递归;
public class BaHuangHou {
//定义一个max表示共有多少个皇后
int max = 8;
//定义一个数组,保存皇后放置位置的结果,
int[] array = new int[max];
static int count = 0;//统计摆放方式的个数
public static void main(String[] args) {
BaHuangHou queue8 = new BaHuangHou();
queue8.check(0);//先放第一个皇后,到第一行第一列,之后就递归实现各个皇后的摆放
System.out.printf("一共有%d种解法",count);
}
//1. 实现放置第n个皇后(n从0开始)
private void check(int n) {
if(n == max) {//说明在放第九个皇后,因为n从0开始,此时n=8,八个皇后已经放好了
print();
return;
}
else {//依次放入皇后,并判断是否冲突
for(int i=0;i<max;i++) {
//先把这个皇后放在该行的第一列
array[n] = i;
//判断当放置第n个皇后到i列时,是否冲突
if(judge(n)) {
//不冲突,接着放第n+1个皇后,开始递归
check(n+1);
}
//如果冲突,就继续执行for循环的i++,再array[n] = i;将第n个皇后放在本行的后移的位置
}
//递归放在for循环,check是每一次递归都有这个for循环,里面有回溯,就是放到某一个时,发现原先选择不优或不符合要求,就会退回上一步重新选择,这种走不通就退回再走,就是回溯
//在找到一个正确解后,就会挥到上一个栈,而这个栈里面的for会继续寻找,运行完for循环可能找到了其他摆放方式
}
}
//2. 查看放置第n个皇后时(n从0开始),检测该皇后是否和已经摆放的皇后冲突
private boolean judge(int n) {
for(int i=0;i<n;i++) {
//array[i]==array[n] 判断第n个皇后与已经摆放的皇后在同一列
//Math.abs(n-i)==Math.abs(array[n]-array[i]) 判断第n个皇后与第i个皇后在同一斜线 行间距等于列间距,在同一斜线
if(array[i]==array[n] || Math.abs(n-i)==Math.abs(array[n]-array[i])) {
return false;
}
}
return true;
}
//3.实现将皇后放置位置输出,遍历数组
private void print() {
count++;
for(int i=0;i<array.length;i++) {
System.out.print(array[i]+" ");
}
System.out.println();
}
//输出:
0 4 7 5 2 6 1 3
0 5 7 2 6 3 1 4
0 6 3 5 7 1 4 2
0 6 4 7 1 3 5 2
1 3 5 7 2 0 6 4
1 4 6 0 2 7 5 3
1 4 6 3 0 7 5 2
1 5 0 6 3 7 2 4
1 5 7 2 0 3 6 4
1 6 2 5 7 4 0 3
1 6 4 7 0 3 5 2
1 7 5 0 2 4 6 3
2 0 6 4 7 1 3 5
2 4 1 7 0 6 3 5
2 4 1 7 5 3 6 0
2 4 6 0 3 1 7 5
2 4 7 3 0 6 1 5
2 5 1 4 7 0 6 3
2 5 1 6 0 3 7 4
2 5 1 6 4 0 7 3
2 5 3 0 7 4 6 1
2 5 3 1 7 4 6 0
2 5 7 0 3 6 4 1
2 5 7 0 4 6 1 3
2 5 7 1 3 0 6 4
2 6 1 7 4 0 3 5
2 6 1 7 5 3 0 4
2 7 3 6 0 5 1 4
3 0 4 7 1 6 2 5
3 0 4 7 5 2 6 1
3 1 4 7 5 0 2 6
3 1 6 2 5 7 0 4
3 1 6 2 5 7 4 0
3 1 6 4 0 7 5 2
3 1 7 4 6 0 2 5
3 1 7 5 0 2 4 6
3 5 0 4 1 7 2 6
3 5 7 1 6 0 2 4
3 5 7 2 0 6 4 1
3 6 0 7 4 1 5 2
3 6 2 7 1 4 0 5
3 6 4 1 5 0 2 7
3 6 4 2 0 5 7 1
3 7 0 2 5 1 6 4
3 7 0 4 6 1 5 2
3 7 4 2 0 6 1 5
4 0 3 5 7 1 6 2
4 0 7 3 1 6 2 5
4 0 7 5 2 6 1 3
4 1 3 5 7 2 0 6
4 1 3 6 2 7 5 0
4 1 5 0 6 3 7 2
4 1 7 0 3 6 2 5
4 2 0 5 7 1 3 6
4 2 0 6 1 7 5 3
4 2 7 3 6 0 5 1
4 6 0 2 7 5 3 1
4 6 0 3 1 7 5 2
4 6 1 3 7 0 2 5
4 6 1 5 2 0 3 7
4 6 1 5 2 0 7 3
4 6 3 0 2 7 5 1
4 7 3 0 2 5 1 6
4 7 3 0 6 1 5 2
5 0 4 1 7 2 6 3
5 1 6 0 2 4 7 3
5 1 6 0 3 7 4 2
5 2 0 6 4 7 1 3
5 2 0 7 3 1 6 4
5 2 0 7 4 1 3 6
5 2 4 6 0 3 1 7
5 2 4 7 0 3 1 6
5 2 6 1 3 7 0 4
5 2 6 1 7 4 0 3
5 2 6 3 0 7 1 4
5 3 0 4 7 1 6 2
5 3 1 7 4 6 0 2
5 3 6 0 2 4 1 7
5 3 6 0 7 1 4 2
5 7 1 3 0 6 4 2
6 0 2 7 5 3 1 4
6 1 3 0 7 4 2 5
6 1 5 2 0 3 7 4
6 2 0 5 7 4 1 3
6 2 7 1 4 0 5 3
6 3 1 4 7 0 2 5
6 3 1 7 5 0 2 4
6 4 2 0 5 7 1 3
7 1 3 0 6 4 2 5
7 1 4 2 0 6 3 5
7 2 0 5 1 4 6 3
7 3 0 2 5 1 6 4
一共有92种解法
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现