数据结构(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次