递归应用场景

迷宫问题(回溯), 递归(recursion)
image.png

递归的概念

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

递归调用机制

列举两个小案例,来帮助大家理解递归,如果学习过了,就回顾一下,没学习过,就好好看

  1. 打印问题
  2. 阶乘问题

image.png

案例

  1. package com.dance.recursion;
  2. public class Demo {
  3. public static void main(String[] args) {
  4. test(3);
  5. // 阶乘案例 1 x 2 x 3 = 6
  6. System.out.println(factorial(3));
  7. }
  8. /**
  9. * 递归案例 1
  10. * @param n 数字
  11. */
  12. public static void test(int n) {
  13. if(n > 2) {
  14. // 递归位置
  15. test(n - 1);
  16. }
  17. System.out.println("n = " + n);
  18. }
  19. /**
  20. * 递归案例 2
  21. * @param n 阶乘数
  22. * @return 阶乘数
  23. */
  24. public static int factorial(int n){
  25. if(n==1){
  26. return 1;
  27. }
  28. return factorial(n - 1) * n;
  29. }
  30. }

执行结果

n = 2
n = 3
6

递归能解决什么样问题

递归用于解决什么样的问题

  1. 各种数学问题如: 8皇后问题, 汉诺塔问题, 阶乘问题, 迷宫问题, 球和篮子问题(Google编程大赛)
  2. 各种算法中也会使用到递归, 比如快速排序, 二分查找,分治算法等
  3. 将用栈解决的问题 -> 递归代码比较简洁

    递归需要遵守的重要规则

  4. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)

  5. 方法的局部变量是独立的, 不会相互影响, 比如n变量
  6. 如果方法中使用的是引用类型的变量(比如数组), 就会共享该引用类型的数据
  7. 递归必须向退出递归的条件逼近, 否则就是无线递归了, 出现StackOverflowError(栈溢出).死龟了: )
  8. 当一个方法执行完毕,或者遇到return ,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕

    递归-迷宫问题(回溯算法)

    迷宫问题

    image.png

    代码实现

    ```java package com.dance.recursion;

import java.util.Arrays;

/**

  • 迷宫问题 */ public class Maze {

    public static void main(String[] args) {

     int[][] maze = createMaze(8, 7);
     setAppointWall(maze, 3, 1);
     setAppointWall(maze, 2, 2);
     setAppointWall(maze, 3, 2);
     setAppointWall(maze, 5, 2);
     setAppointWall(maze, 5, 3);
     setAppointWall(maze, 5, 4);
     setAppointWall(maze, 5, 5);
     // 初始迷宫
     System.out.println("初始迷宫");
     showMaze(maze);
     boolean b = setWay(maze, 1, 1, 6, 5);
     System.out.println("路线");
     // 找到路线 后的
     showMaze(maze);
    

    }

    /**

    • 使用递归回溯来给小球找路
    • 约定 当maze[startX][startY]
    • case 0:
    • 表示该点没有走过
    • case 1:
    • 表示墙 不能走
    • case 2:
    • 表示可以走
    • case 3:
    • 表示该点已经走过,但是走不通
    • 策略
    • 下 -> 右 -> 上 -> 左 *
    • @param maze 迷宫
    • @param x 起始x坐标
    • @param y 起始y坐标
    • @return 是否找到, 找到返回true,没找到返回false */ public static boolean setWay(int[][] maze, int startX, int startY, int endX, int endY) { // 断言 不能从墙壁开始 if (maze[startX][startY] == 1 || maze[endX][endY] == 1) {

       // 墙壁
       return false;
      

      } if (maze[endX][endY] == 2) {

       // 表示已经走通了
       return true;
      

      } else {

       if (maze[startX][startY] == 0) {
           // 没有走过 将当前节点设置为2
           maze[startX][startY] = 2;
           // 向下走 x 轴 + 1
           if (setWay(maze, startX + 1, startY, endX, endY)) {
               // 如果能走通
               return true;
               // 向右走 y 轴 + 1
           } else if (setWay(maze, startX, startY + 1, endX, endY)) {
               return true;
               // 向上走 x 轴 - 1
           } else if (setWay(maze, startX - 1, startY, endX, endY)) {
               return true;
               // 向左走 y 轴 - 1
           } else if (setWay(maze, startX, startY - 1, endX, endY)) {
               return true;
           } else {
               // 上下左右都不能走 说明该点走不通
               maze[startX][startY] = 3;
               return false;
           }
       }else{
           // 如果不是0, 可能是 2, 3
           // 直接返回false
           return false;
       }
      

      } }

      /**

    • 创建迷宫 *
    • @param width 宽度
    • @param height 高度
    • @return 迷宫 */ public static int[][] createMaze(int width, int height) { int[][] maze = new int[width][height]; setWall(maze); return maze; }

      /**

    • 指定位置生成墙壁 *
    • @param maze 迷宫
    • @param x x坐标
    • @param y y坐标 */ public static void setAppointWall(int[][] maze, int x, int y) { maze[x][y] = 1; }

      /**

    • 将迷宫的上下设置为墙壁, 使用1表示 *
    • @param maze 迷宫 */ private static void setWall(int[][] maze) { // 将上下设置为墙壁 Arrays.fill(maze[0], 1); Arrays.fill(maze[maze.length - 1], 1); // 将左右设置为墙壁 for (int i = 0; i < maze.length; i++) {

       maze[i][0] = 1;
       maze[i][maze[i].length - 1] = 1;
      

      } }

      /**

    • 展示迷宫 *
    • @param maze 迷宫 */ public static void showMaze(int[][] maze) { for (int[] ints : maze) {
       for (int anInt : ints) {
           System.out.print(anInt + "\t");
       }
       System.out.println();
      
      } }

}

执行结果
```java
初始迷宫
1    1    1    1    1    1    1    
1    0    0    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    1    1    1    1    1    
1    0    0    0    0    0    1    
1    1    1    1    1    1    1    
路线
1    1    1    1    1    1    1    
1    2    2    2    3    3    1    
1    3    1    2    3    3    1    
1    1    1    2    3    3    1    
1    2    2    2    3    3    1    
1    2    1    1    1    1    1    
1    2    2    2    2    2    1    
1    1    1    1    1    1    1

对迷宫问题的讨论

  1. 小球得到的路径,和程序设置的找路策略有关,就是先上还是先下…
  2. 在得到小球的路径时,可以使用不同的策略,看看路径是不是有变化
  3. 测试回溯现象
  4. 思考: 如何求出最短路径? 思路 -> 代码实现

使用不同的策略,然后使用一个Map记录策略和步长,然后对比找出最优策略和最短路径

递归-八皇后问题(回溯算法)

八皇后问题

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例,该问题是国际西洋棋棋手马克思·贝瑟尔于1848年提出: 在8X8的国际象棋上摆放8个皇后,使其不能互相攻击, 即: 任意两个皇后不能处于同一行,同一列,同一条斜线上,问:有多少种摆法(92)
image.png

思路分析

  1. 第一个皇后先放第一行,第一列
  2. 第二个皇后放在第二行第一列,然后判断是否ok, 如果不ok, 继续放在第二列, 第三列,依次吧所有列都放完找到一个合适的
  3. 继续第三个皇后还是第一列,第二列……直到第八个皇后也能放在一个不冲突的位置,算是找到了一个正确的解
  4. 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即,将第一个皇后,放到第一列的所有正确解,全部得到
  5. 然后回头第一个皇后放第二列,后面继续循环1,2,3,4的步骤

    示意图

    image.png
    说明
    理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题,arr[8] = {0,4,7,5,2,6,1,3} // 对应arr下标 表示第几行, 即第几个皇后 arr[i] = val, val表示第i+1个皇后,放在第i+1行的第val+1列

    代码实现

    ``` package com.dance.recursion;

import java.util.Arrays;

public class EightQueens {

/**
 * 代表有8个皇后
 */
static int maxSize = 8;

/**
 * 定义数组保存皇后位置的结果
 * 例如: arr = {0,4,7,5,2,6,1,3}
 * 下标 + 1 代表行(从下往上数)
 * 值 + 1 代表列(从左往右数)
 */
int[] arr = new int[maxSize];

/**
 * 统计解法数量
 */
static int count = 0;

/**
 * 检查次数
 */
static int judgeCount = 0;

public static void main(String[] args) {
    EightQueens eightQueens = new EightQueens();
    eightQueens.checked(0);
    System.out.println("总共有" + count + "中解法");
    System.out.println("检查方法调用次数:" + judgeCount);
}

/**
 * 输出皇后摆放的位置
 */
public void print() {
    System.out.println(Arrays.toString(arr));
}

/**
 * 判断放置的第n个皇后是否和之前摆放的皇后冲突
 *
 * @param n 第n个皇后
 * @return 是否冲突, 冲突返回false, 不重复返回true
 */
private boolean judge(int n) {
    judgeCount++;
    // 循环之前的皇后
    for (int i = 0; i < n; i++) {
        // 取到前面的皇后
        int i1 = arr[i];
        // i1 == arr[n] 代表y轴冲突
        // Math.abs(n - i) == Math.abs(arr[n] - arr[i]) 代表斜轴冲突
        /*
         * abs : 求绝对值
         *  第一个皇后           第二个皇后第一次     第二个皇后第二次      第二个皇后第三次
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0    0 1 0 0 0 0 0 0    0 0 1 0 0 0 0 0
         *  1 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0
         *  第三个皇后第一次      第三个皇后第二次     第三个皇后第三次      第三个皇后第四次
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0    0 0 0 0 0 0 0 0
         *  1 0 0 0 0 0 0 0    0 1 0 0 0 0 0 0    0 0 1 0 0 0 0 0    0 0 0 1 0 0 0 0
         *  0 0 1 0 0 0 0 0    0 0 1 0 0 0 0 0    0 0 1 0 0 0 0 0    0 0 1 0 0 0 0 0
         *  1 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0    1 0 0 0 0 0 0 0
         *  第三个皇后第五次
         *  0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0
         *  0 0 0 0 0 0 0 0
         *  0 0 0 0 1 0 0 0
         *  0 0 1 0 0 0 0 0
         *  1 0 0 0 0 0 0 0
         * 第一个皇后放在了 arr = {0,.....} 的位置 也就是第一行,第一列
         * 所以 判断 i < 0 不会走循环 直接可以放,因为当前棋盘上是空的
         * 第二个皇后,第一次放在arr = {0,0,.....}的位置, 也就是第二行,第一列
         * [1]所以 判断 i < 1 所以会走一次循环
         * 开始走逻辑
         * arr[i] == arr[n] => 0 == 0 条件成立 代表在同一列所以不能放
         * 第二个皇后,第二次会放在arr = {0,1,.....}的位置, 也就是第二行,第二列
         * ^[1]
         * 0 == 1 条件不成立, 代表不在同一列, 继续走下一个条件
         * Math.abs(n - i) == Math.abs(arr[n] - arr[i]) => Math.abs(1 - 0) == Math.abs(1 - 0)
         * 计算Math.abs后 => 1 == 1 条件成立 代表在同一斜线所以不能放
         * 第二个皇后,第三次会放在arr = {0,2,.....}的位置, 也就是第二行,第三列
         * ^[1]
         * Math.abs(n - i) == Math.abs(arr[n] - arr[i]) => Math.abs(1 - 0) == Math.abs(2 - 0)
         * 计算Math.abs后 => 1 == 2 条件不成立 代表斜线不冲突,可以放
         * 然后开始放第三个皇后,第一次会被放在arr = {0,2,0,.....},也就是第三行, 第一列
         * 判断和第一个皇后同列,不能放(同列判断很好理解就不多写了)
         * 第三个皇后,第二次会被放在arr = {0,2,1,.....}也就是,第三行m,第二列
         * 和第一个皇后不同列, 然后计算斜线
         * Math.abs(n - i) == Math.abs(arr[n] - arr[i]) => Math.abs(2 - 0) == Math.abs(1 - 0)
         * 计算Math.abs后 => 2 == 1 条件不成立 代表斜线不冲突,可以放
         * 但是还要计算和第二个皇后是否同列,同斜线
         * 判断和第二个皇后不同列,然后计算斜线
         * Math.abs(n - i) == Math.abs(arr[n] - arr[i]) => Math.abs(2 - 1) == Math.abs(1 - 2)
         * 计算Math.abs后 => 1 == 1 条件成立 代表斜线冲突,不可以放
         * 第三个皇后第三次会被放在arr = {0,2,2,.....} 也就是第三行,第三列
         * 和第二个同列冲突,不能放
         * 第三个皇后第四次会被放在arr = {0,2,3,.....} 也就是第三行,第四列
         * 和第二个皇后成斜线,不能放
         * 第三个皇后第五次会被放在arr = {0,2,4,....} 也就是第三行第五列
         * 和第一个第二个皇后都不同列同斜线,可以放,后面持续递归..........
         */
        if (i1 == arr[n] || Math.abs(n - i) == Math.abs(arr[n] - arr[i])) {
            return false;
        }
    }
    // 循环完成没有返回FALSE 代表不冲突返回TRUE
    return true;
}

/**
 * 放置第n个皇后
 *
 * @param n 第n个皇后
 */
private void checked(int n) {
    if (n == maxSize) {
        // 代表已经放到了第9个了
        print();
        count++;
        return;
    }
    // 依次放入皇后,并判断是否冲突
    for (int i = 0; i < maxSize; i++) {
        // 先把当前的皇后 n 放到第一列 0
        arr[n] = i;
        // 判断当放置第n个皇后到第i列时,是否冲突
        if (judge(n)) {
            // 不冲突 开始递归放n + 1个 -> 一直到底9个停
            // 如果放不到第9个,也就是放完,就会回溯上一个棋子后移后继续递归,一直到放完8个棋子
            checked(n + 1);
        }
        // 冲突 , 循环进入下一个位置
    }
}

}

执行结果

[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中解法 检查方法调用次数:15720 ```