第05篇 递归

一、什么是递归

简单的说,程序调用自身的编程技巧称为递归( recursion)

递归会遵守如下准则:

  1. 每执行一次方法,就会创建一个新的受保护的独立空间(栈空间)
  2. 方法的局部变量是独立的,不会相互影响
  3. 如果方法的参数是引用类型的,如数组,那么会共享这个变量
  4. 递归必须有退出条件,而且需要向退出递归的条件逼近,否则就会无限递归,出现StackOverflowError
  5. 当一次方法执行完毕或者是return之后,就会返回到调用它的地方

其中第4个算是递归的一个必要条件,而其他的,算是递归的特征
例如:求n的阶乘

  public static int factorial(int n) {
    if (n == 1) {
      return 1;
    }
    return n * factorial(n - 1);
  }

可以看到,这里的退出条件是n==1,而会不停的通过调用 factorial(n - 1)来逼近1,而如果是 factorial(n + 1),那么这个递归就不会结束了

二、递归的调用过程

这里还是以n的阶乘来讲解递归过程,如计算4的阶乘,我们可以在main函数里面调用函数计算:

  public static void main(String[] args) {
    int result=factorial(4);
    System.out.println(result);
  }

计算的调用过程如下:
在这里插入图片描述

三、小球回溯

(一)问题描述

在如图所示的一个n行m列迷宫里面,某一个地方有一个小球,还有一些障碍物,请找出一条达到出口的路线(出口在右下角)
在这里插入图片描述
我对原始问题做了以下变动:

  1. 行数和列数可以变化
  2. 小球的位置随机生成
  3. 障碍物随机生成(四周的除外)
  4. 如果没有一条路径,就打印没有路径

(二)思路分析

迷宫图的生成

为了能够很好的表示迷宫图,这里定义一个对象来表示迷宫图:

  @Data
  @AllArgsConstructor
  @NoArgsConstructor
  public static class Map {

    private int startX;
    private int startY;
    private int[][] map;
  }

其中startX表示小球起点所在行;startY表示小球起点所在列(都是从下标0开始);map表示整个地图,有障碍物的地方为1,没有障碍物的地方是0,小球起点位置为9,例如其中一个地图为:

1 1 1 1 1 1 1 1 
1 0 0 0 0 0 0 1 
1 1 1 1 1 9 0 1 
1 1 0 0 1 1 0 1 
1 0 0 1 0 0 1 1 
1 0 0 0 0 0 0 1 
1 0 0 0 0 1 0 1 
1 1 1 1 1 1 1 1 

此时startX为2,startY为5

  1. 小球位置的生成
    使用随机数即可
  2. 障碍物的生成
    四周的设置成1,然后再随机一定数量的位置出来,设置成1
  3. 相应的方法名为:generateMap

查找路线

  1. 对于小球移动到的每一个位置,对下一步按照下、右、上、左这种策略来前进,也就是说先尝试向下走,如果走不通就尝试向右边走等等。如果某个方向走通了,那么这个位置需要标志成走过了(用2记录),如果四个方向都不可以走通,证明这个点是不能走的,需要进行标记,并回到上一步;
  2. 从起点位置开始走;所以初始的时候,把map[startX][startY] 标志成0,等找到路线,再重新设置成9,方便知道起点是哪里;
  3. 假如小球当前所在的位置为map[x][y],向下走其实就是x+1,向右走是y+1,向上是x-1,向左边走是y-1

(三)代码实现

package com.firewolf.javabase.s005_recursion.realcase;

import java.util.Random;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 迷宫问题
 * 问题描述:
 * 在一个迷宫里面,某一个地方有一个小球,还有一些障碍物,请找出一条达到出口的路线(出口在右下角)
 */
public class Maze {


  @Data
  @AllArgsConstructor
  @NoArgsConstructor
  public static class Map {

    private int startX;
    private int startY;
    private int[][] map;
  }


  public static void findWay(int[][] map, int x, int y) {
    map[x][y] = 0; //走之前,把起点位置标成0,否则第一步走不出去
    boolean b = goOneStep(map, x, y);
    if (!b) {
      System.out.println("没有可以走的路....");
    }
    map[x][y] = 9; //走完之后,恢复起点位置标志,方便查看
  }

  /**
   * 走一步
   * @param map
   */
  public static boolean goOneStep(int[][] map, int x, int y) {

    int mapRows = map.length; //map的行数,比小球能走的多两行(墙)
    int mapCols = map[0].length; //map的列数,比小球能走的多两行(墙)

    if (map[mapRows - 2][mapCols - 2] == 2) { //表明找到了
      return true;
    }
    if (map[x][y] == 0) {//当前这个位置没有走过

      //走这个点
      map[x][y] = 2;

      //接下来往其他方向走,按策略 下、右、上、左 走,如果走不通,都走不通就会回到上一步;
      if (goOneStep(map, x + 1, y)) {
        return true;
      } else if (goOneStep(map, x, y + 1)) {
        return true;
      } else if (goOneStep(map, x - 1, y)) {
        return true;
      } else if (goOneStep(map, x, y - 1)) {
        return true;
      } else {
        //上下左右都行不通,证明这个点走不了
        map[x][y] = 3;
        return false;
      }
    } else { //已经走过了
      return false;
    }
  }

  /**
   * 产生地图
   * @param rows 行数
   * @param colums 列数
   */
  public static Map generateMap(int rows, int colums) {
    //教研输入是否正确
    assert rows > 0 && colums > 0;

    //初始化数组
    int[][] mapArry = new int[rows + 2][colums + 2];

    //把四周围上墙,就是把赋值为1,默认是0 的表示可以走的路
    for (int i = 0; i < rows + 2; i++) {
      mapArry[i][0] = 1;
      mapArry[i][rows + 1] = 1;
    }

    for (int i = 0; i < colums + 2; i++) {
      mapArry[0][i] = 1;
      mapArry[colums + 1][i] = 1;
    }

    Map map = new Map();

    //随机生成起点位置,用2标志
    Random random = new Random();
    int x = 1 + random.nextInt(rows);//行标
    int y = 1 + random.nextInt(colums); //列标
    mapArry[x][y] = 9;

    map.startX = x;
    map.startY = y;

    //随机生成障碍物(可以走的1/3个吧)
    int count = rows * colums / 3;
    for (int i = 0; i < count; i++) {
      x = 1 + random.nextInt(rows);//行标
      y = 1 + random.nextInt(colums); //列标
      if (mapArry[x][y] != 9 && !(x == rows && y == colums)) {
        mapArry[x][y] = 1;
      }
    }

    map.map = mapArry;
    return map;
  }


  public static void printMap(int[][] map) {
    System.out.println("-----------------------");
    for (int i = 0; i < map.length; i++) {
      for (int j = 0; j < map[0].length; j++) {
        System.out.printf("%d ", map[i][j]);
      }
      System.out.println();
    }
    System.out.println();
    System.out.println();
    System.out.println();
  }

}

四、八皇后

(一)问题描述

皇后能横向,纵向和斜向移动,在这三条线上的其他棋子都可以被吃掉。所谓八皇后问题就是:将八位皇后放在如下图所示的一张8x8的棋盘上,使得每位皇后都无法吃掉别的皇后,(即任意两个皇后都不在同一条横线,竖线和斜线上),问一共有多少种摆法。
在这里插入图片描述
此问题是在1848年由棋手马克思·贝瑟尔提出的,后面陆续有包括高斯等大数学家们给出自己的思考和解法,所以此问题不只是有年头了,简直比82年的拉菲还有年头,我们今天不妨尝尝这老酒;

其实这个问题可以扩展成为在一个n*n的棋盘上进行求解;

(二)思路分析

  1. 通过一个数组positions = new int[n] 来存放一种可能的结果
    positions[n-1]=i 表示将第n个皇后摆放在第n行的第i+1列
    行从下往上数,列从左往右数
  2. 我们从第一个皇后开始摆放,没问题的话就开始摆放第二个皇后,如果摆放到了第n+1个,那么证明所有的n个皇后都摆放完了,那么就意味着有一种解法了;
  3. 对于第n个皇后,我们可以放在第n-1行的0到n列,放完之后,我们需要进行判断这个位置可不可以的,如果不与前面的冲突,就可以继续放置第n+1个皇后;
  4. 判断是否冲突:其实也就是判断第n个与前面的n-1个是否冲突;
    ①. 行:由于第n个会放在第n-1行,所以这些皇后不可能在同一行,不用判断;
    ②. 列:由于第第n个元素放置的列为positions[n-1],所以通过比较与前面几个数组值是否一致既可;
    ③.是否在同一斜线
    对于任何两个皇后n和i+1个皇后,满足如下条件:
    在这里插入图片描述
    如果所示,第n个皇后的坐标为:(positions[n-1],n-1),而第i+1个皇后的坐标为:(positions[i],i),要满足在一条斜线上,则红线长度和绿线商都一样,有两种情况:
    a. 第n个在第i+1个的右边,则:positions[n-1]-positions[i]=n-1-i;
    b. 第n个在左边,则:positions[i]-positions[n-1]=n-1-i
    总之,需要满足:Math.abs(n - 1 - i) == Math.abs(positions[n - 1] - positions[i])

(三)代码实现

package com.firewolf.javabase.s005_recursion.realcase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 八皇后
 *
 */
public class QueueN {

  private static int[] positions = null;

  private static int count = 0;

  private static List<int[]> result = new ArrayList<>();

  public static List<int[]> queueN(int n) {
    positions = new int[n];
    position(1, n);
    return result;
  }

  /**
   * 摆放第n个换后
   * @param n
   */
  public static void position(int n, int total) {
    if (n > total) { // n 已经大于最大皇后数量了,就证明OK了
      result.add(Arrays.copyOf(positions, total));
      count++;
      return;
    }

    for (int i = 0; i < total; i++) {
      positions[n - 1] = i; //让第n个皇后放在第n-1行的各个位置进行测试
      if (check(n)) {
        position(n + 1, total);
      }
    }
  }

  //检查第n个有没有问题
  private static boolean check(int n) {
    for (int i = 0; i < n - 1; i++) {
      //判断在同一列
      if (positions[i] == positions[n - 1]
          //判断是不是在一条斜线,在同一条斜线的有两类:
          //1. 右上到左下,45度角,
          //2. 左上到右下,135,但是不管怎么样,两条边的长度一样,水平方向边长:Math.abs(positions[n - 1] - positions[i])
          // 垂直方向边长Math.abs(n - 1 - i)
          || Math.abs(n - 1 - i) == Math.abs(positions[n - 1] - positions[i])

      ) {
        return false;
      }
    }
    return true;
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值