场景分析
迷宫回溯问题:
在上面这个迷宫中,小球需要绕过禁区(叉叉),从左上角走到右下角的出口,对于人来说可能一眼就能发现怎么走能到终点,但是对于计算机来说,它需要根据某种策略去进行尝试,最终才能抵达出口
基本介绍
递归其实就是方法自己调用自己,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题,同时可以让代码变得更简洁;
递归回顾
其实我们在日常编码中已经用到过递归了,但是我们可能没有注意到,现在我们用下面几行简单的代码一起回顾一下:
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);
}
上面这个就是一个典型的递归,因为我们可以发现在test方法中,在if判断条件中,它对变量进行二次计算后再次调用自己这个方法本身;
按照道理来说上面代码的打印顺序就应该是 4 3 2 1,但是我们来看它实际的打印:
我们可以发现,我们最开始传入的4却是最后打印的,这是为什么呢?
我们来看一下这个方法在底层JVM中的变化是什么样的:
流程分析
1.当程序执行到一个方法的时候,就会开辟一个独立的栈空间;
2.在主方法里面调用test(4)的时候,在栈底就开辟了一个新的空间
3.但是在该方法中判断n>2,又调用了本方法test(3),那么这个时候test(4)的打印就不会再执行,就直接再开辟一个栈test(3);
4.以此,一直到开辟test(2)的时候,不再调用新的方法,直接执行打印;
5.这个时候说明test(2)真正执行完了,test(2)这个栈也就消失了,然后再执行test(3)的打印,然后依次往下,最终到主方法的test(4)执行完毕则退出程序,最终输出顺序也就是:2 3 4
与普通循环的对比:
public static void main(String[] args) {
test();
}
public static void test( ){
int n = 5;
for (int i = 0; i < n; i++) {
if (n>=2){
n--;
}
System.out.println("n="+n);
}
}
上面就是普通的循环打印,那么它就是正常的顺序执行
递归能解决的问题
1.各种数学问题,例如:八皇后问题、汉诺塔问题、阶乘问题、迷宫问题、球和篮子的问题;
2.各种算法中也会使用到递归,比如快排、归并排序、二分查找、分治算法等;
3.将用栈解决的问题=>递归代码比较简洁;
递归规则
1.执行一个方法时,就创建一个新的受保护的独立空间(栈空间);
2.方法的局部变量是独立的,不会相互影响;如果方法中使用的是引用类型(比如数组)的变量,就会共享该引用类型的数据,因为该数据会在堆内存中被创建出来;
3.递归必须向退出递归的条件逼近,否则就是无限递归出现StackOverflowError,栈内存溢出错误,死龟了;
4.当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就真正执行完毕;
迷宫问题
回到我们最开始的迷宫问题
要求
1.小球得到的路径,和程序员设计的找路策略有关,即路的上下左右的顺序相关;
2.再得到小球路径的时候,可以先使用(下右上左),再改成(上右下左),看看路径是不是有变化;
3.测试回溯现象;
流程分析
1.先创建一个二维数组用来模拟迷宫
2.将第一行和最后一行以及第一列和最后一列全部设置为禁区(设置成1),再设置个别的挡板禁区;
3.使用递归回溯来给小球找路;
4.从地图的(1,1)位置开始,如果能够走到map[6] [5]的位置,说明通路找到
5.当走到位置数据为0时,说明该点还没有走过,如果为1则表示禁区(不能走),如果为2则说明是通路可以走,如果为3表示该点已经走过,但是走不通,下一次回溯的时候就不再去走这条路了;
6.在走迷宫时,需要确定一个策略,我们假定一个策略是下->右->上->左,如果该点走不通再回溯;
代码实现
public static int[][] getLabyrinth(){
//创建一个二维数组用来模拟迷宫
int [][] map = new int[8][7];
//将第一行和最后一行全部设为禁区;
for (int i = 0; i < 7; i++) {
map[0][i]=1;
map[7][i]=1;
}
//将第一列和最后一列全部设为禁区;
for (int i = 0; i < 8; i++) {
map[i][0]=1;
map[i][6]=1;
}
//设置个别禁区
map[3][1]=1;
map[3][2]=1;
setWay(map, 1, 1);
//输出新的地图(小球探测并标识后的地图):
return map;
}
/**
* @Author:Strine
* @param map:地图
* @param i :
* @param j :从地图的第i行,第j列开始(1,1)
* @return 如果找到通路则返回true,否则返回false,能到map[6][5]的位置,则说明通路找到
* 约定:当map[i][j]为0时,说明该点还没有走过,当为1的时候表示禁区(不能走),如果为2则说明是通路可以走 ,如果为3表示该点已经走过但是走不通
* */
public static boolean setWay(int [][]map,int i,int j){
if (map[6][5]==2){ //说明通路已经找到了;
return true;
}else {
if (map[i][j]==0){
//先看当前这个点没有走过(为0),则按照策略走(下->右->上->左)
//将该点设置为2,也就是假定该点是能走通的;
map[i][j]=2;
if (setWay(map,i+1,j)){ //如果按照这个点往下走,如果能走通则返回true,如果走不通则尝试向右走,
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; //==》直到尝试向左走,如果还是走不通,说明该店是走不通的,则将当前这个点重新设置为3
return false;
}
}else {
//那么如果当前这个点没有走过
//如果当前这个点为1,说明为禁区不能走
//如果当前点为2说明有人在走
//如果当前点为3,说明有人走过了,因此直接返回false
return false;
}
}
}
效果展示
流程简述
1.传入map[1] [1]
2.该位置为0,则将该位置设置为2,进行第一次递归调用,往下走;
3.下面这个位置依旧为0,因此将其设置为2,进行第二次递归调用,往下走;
4.第二次递归调用发现下面这个位置为禁区,则返回false,回到第二次递归调用,进行往右走的递归调用(第三次);
5.发现右边位置为0,因此将其设置为2,进行第四次递归调用,往下走;
6.发现下面的位置又是禁区,则返回false回到第四次递归调用,进行往右走的递归调用(第五次)
7.接下来就是一连串的四次递归调用(都是往下走成功了)
8.以此类推,后面几次调用都是发现下面的位置为禁区,然后回去一次递归往右走,最终走到出口返回true,然后从最后那个栈往前依次返回true,依次销毁 ;
测试回溯现象
我们在设置禁区的时候,再在右边加一个挡板,将它的路给堵死;
1.我们发现刚开始map[1] [1]位置的时候,该位置设置为2,然后第一次递归调用往下走的时候也先设置为2;
2.但是下面这个位置进行下右上左的递归调用都为false,说明都走不通,则将当前位置设置为3,表示走不通,并返回false
3.因此发生了回溯,回到上一个位置map [1] [1],然后它就往右上左进行递归调用,发现也都为false,最终它也将自身修改为3,最终返回false
修改路径策略
将下->右->上->左的路径策略修改为上->右->下->左;
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;
其实也就是修改这几个递归调用的顺序;
效果展示