数据结构(Java)学习笔记——知识点:递归

九、递归(Recursion)

1、递归实际应用场景

 下面我们来看一个递归的实际应用场景:迷宫问题(回溯)。
 游戏规则:红色方块是墙,小球不能走动,白色方块小球随意走动,现在让小球,左上角移动到右下角箭头处。此时就用得到递归。
   在这里插入图片描述

2、递归的概念

 递归就是方法自己调用自己,每次调用时传入不同的变量,递归能使编程者解决复杂的问题,同时可以让代码变得简洁。

3、递归调用机制

 这里写出两个小案例:打印问题阶乘问题

1)、打印问题

 使用递归来打印输出2到n的值。
 如下图所示:当代码执行到一个方法的时候,java虚拟机会在内存内,自动创建一个栈空间,并且里面调用方法中的代码段。
 在我们这个程序中,代码从上往下执行,执行到main()方法的时候,栈中为main开辟了一个空间(图中浅蓝色区域),当代码继续执行,会执行到 test(4) ,此时栈中又会为 test(4) 生成一个栈空间(图中红色区域),再走 if 判断,n > 2,所以执行到 test(n - 1) ,于是又为 test(3) 开辟一个栈空间(图中深蓝色区域),继续走 if 判断,n 还是大于 2,于是继续走test(n - 1),于是又为 test(2) 开辟了一个栈空间(图中绿色区域),此时继续执行,if 走不通,走到了 println 方法,此时控制台输出 n = 2 ,然后这个绿色栈区域的代码就执行完了,空间被回收掉,回到深蓝色的栈区域,控制台输出 n = 3,空间被回收掉。以此类推,直到回到 main 栈(浅蓝色栈区域),此时代码全部执行完成,退出程序。
在这里插入图片描述

代码实现:

public class print {
    public static void main(String[] args) {
        test(4);//设置初始n的值为4
    }

   public static void test(int n){
       if(n > 2){
           test(n - 1);//每一次递归调用n的值都减1
       }
       //每次调用到test时就会暂时不执行该语句,等最后一次得到结果以后,从最后一次的结果倒序向前输出
       System.out.println("n = " + n);
    }
}

运行结果:

n = 2
n = 3
n = 4

2)、阶乘问题

 使用递归来计算一个大于等于1的数的阶乘。
 代码运行思路参照上一个例子,换汤不换药。
 
代码实现:

public class factorial {
    public static void main(String[] args) {
        System.out.println("5的阶乘为:" + factorial1(5));//计算5以内的阶乘
    }

    public static int factorial1(int n){
        if(n == 1){
            return 1;
        }else {
            //每次都调用本函数,直到n = 1时,往回逐渐返回。
            return factorial1(n-1) * n;
        }
    }
}

运行结果:

5的阶乘为:120

 思路带入代码中,自己模拟跑一下,就清晰明了了。

4、递归能解决的问题

 1)、各类数学问题:8皇后问题、汉诺塔、阶乘问题、迷宫问题、球和篮子的问题(Google编程大赛)。
 2)、各种算法中也会使用到递归,比如快速排序、归并排序、二分查找、分治算法等。
 3)、可以把用栈解决的问题,转化为递归来简化代码。

5、递归需要遵守的规则

 1)、执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
 2)、方法的局部变量是独立的,不会相互影响。
 3)、如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据。
 4)、递归必须向退出递归的条件逼近,否则就无限递归。即StackOverflowError报错。
 5)、当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用就把结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕了。

6、实例:迷宫问题

 现在我们来思考一下该如何实现本文一开始就提出的迷宫问题呢?这里有一些说明:
在这里插入图片描述
 我们进行一下头脑风暴,墙为1,走过的为2,没走过的路为0,那么每遇到一个不为0的点,就把他标记成2,然后再根据下右上左的顺序来考虑下一个点是否能走通,每次都调用该方法来判断是否能走通,能走通就返回true,走不通就返回false。这里比较抽象,建议带入代码中,进行理解。

代码实现

package com.database.recursion;

public class Labyrinth {

    public static void main(String[] args) {
        //先创建一个二维数组,模拟迷宫
        //地图,一个八行七列的数组
        int[][] map = new int[8][7];
        //使用1表示墙体
        //上下全部设置为1
        for(int i = 0; i < 7; i++){
            map[0][i] = 1;
            map[7][i] = 1;
        }
        //左右全部设置为1
        for(int j = 0; j < 8; j++){
            map[j][0] = 1;
            map[j][6] = 1;
        }
        //设置挡板
        map[3][1] = 1;
        map[3][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] + "  ");
            }
            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、map表示地图
    //2、i,j表示从地图的哪个位置开始出发(1,1)
    //3、如果小球找到了map[6][5]位置,则说明通路找到了。
    //4、约定,mao[i][j]为 0 表示该点没有走过,为1表示墙,2表示可以走,3表示已经走过但是走不通
    //5、在走迷宫时,需要确定一个策略,下->右->上->左,如果该点走不通,再回溯

    /**
    * @Author: Cui
    * @Description: 用递归给小球找路
    * @DateTime:  12:52
    * @Params: map表示地图,i,j表示从哪里开始找
    * @Return 如果找到了通路就返回true,找不到返回false
    */
    public static boolean setWay(int[][] map, int i,int j){
        if(map[6][5] == 2){
            return true;//如果终点为2,则表示通路已经找到
        }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  1  1  1  1  1  
1  2  2  2  2  2  1  
1  0  0  0  0  2  1  
1  1  1  0  0  2  1  
1  0  0  0  0  2  1  
1  0  0  0  0  2  1  
1  0  0  0  0  2  1  
1  1  1  1  1  1  1

 那么问题就来了,我们怎么做才能求出,小球到达终点的最短路径呢?
 我能想到的是,将每种策略方法都运行一遍,然后每次运行策略方法能跑通的时候都统计一下总共用了多少步,然后将最短路径的那个方案输出。
 这样就复杂了,上右下左,四个方向,排列组合可以总结出2的4次方个方案,也就是16种策略。这里就不过多阐述了。

7、实例:八皇后问题

 八皇后问题是个古老的回溯算法案例。1848年就被提出了,在8*8的国际象棋上拜访八个皇后,使其不能相互攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,文总共有多少种摆法。 这个实例巨不好理解,文字描述可能会让人听不懂。具体看Bilibili上对八皇后问题的动态演示。
来,首先上思路分析:

思路分析

在这里插入图片描述

代码实现

package com.database.recursion;

public class queen8 {
    int max = 8;//定义一共有几个皇后
    int[] array = new int[max];//定义一个数组,存放皇后的位置
    static int count = 0;

    public static void main(String[] args) {
        queen8 queen8 = new queen8();
        queen8.check(0);
        System.out.println("总共打印了" + count + "次");
    }

    //编写一个方法,放置第n个皇后
    public void check(int n){
        if(n == max){//n = 8的时候,其实第8个皇后就已经放好了
            print();
            return;
        }
        //依次放入皇后,并判断是否冲突
        for(int i = 0; i < max; i++){
            //先把这个皇后n,放到该行的第1列
            array[n] = i;
            //判断当放置第n个皇后到i列时,是否冲突
            if(judge(n)){//如果成立,表示不冲突
                //如果不冲突,接着放n + 1个皇后,开始递归
                check(n + 1);
            }
            //如果冲突,就继续执行array[n] = i;即将第n个皇后放置在本行的后移一个位置
        }
    }

    /**
    * @Author: Cui
    * @Description: 检测是否与前面的皇后摆放位置冲突
    * @DateTime:  15:20
    * @Params: n 表示第n个皇后
    * @Return
    */
    //检测是否与前面的摆放位置冲突
    private boolean judge(int n){
        for (int i = 0; i < n; i++){
            if(array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])){//如果当前位置与前面摆放的位置相冲突
                //如果在一条斜线上Math.abs(n - i) == Math.abs(array[n] - array[i])计算的是斜率
                //这里没有必要判断是否在同一行,因为n是一直在递增的,不能能出现n在同一行的情况
                return false;
            }
        }
        return true;
    }

    //写一个方法,将皇后的位置输出
    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
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇直不会放弃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值