从1开始学Java数据结构与算法——递归解决迷宫回溯问题和八皇后问题

递归介绍与分析

案例引入

现在有如下问题,下图是一个很简单的界面,红色区域表示墙,蓝色圆圈表示起始位置,蓝色三角形表示终点位置。那么我们用代码去解决这个问题的话,肯定不能将所有可能的路径用遍历的方法去一个个试,因为这样的话,那这个问题还是相当于我们人为的在解决,而不是计算机去解决,我们只是让计算机显示了我们的方案而已。而且如果遇到更复杂的界面,比如很复杂的迷宫等,那么人为的去遍历写出所有的可能路径也是不现实的。那么这里我们就可以用到递归。——这个也是我们的数据结构与算法里的迷宫回溯问题
在这里插入图片描述

递归机制:

递归简单来讲就是方法自己调用自己,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题,同时可以让代码变得更简洁.。
下面我们用两个问题先来熟悉一下递归和分析一下递归调用的机制
  1)打印问题
  现有如下代码,其实就是一个简单的递归调用方法,我们来分析一下:

public static void main(String args[]) {
		printtest(4);
	}
	
	public static void printtest(int n) {
		if(n>2) {
				printtest(n-1);	
		}
		System.out.println("n="+n);
	}

  我们知道java虚拟机里面可以分为三部分,堆空间、栈空间和代码与常量空间,在执行上诉方法的时候,从main方法开始,最首先会在栈空间中开辟一个区域来存放main方法,因为main方法中调用printtest(4)方法,所以又在栈空间中开辟一个区域用来存放printtest(4)方法。看上面的代码,因为我们的printtest中可以分为if条件和System打印两部分,所以每个printtest方法都会先判断if,那么很自然的根据if中的n是否大于2,printtest(4)又会调用printtest(3),printtest(3)又会调用printtest(2),所以栈空间中从上到下就是如下图所示的四个空间,按照黑色箭头的方向逐级调用
  到这里为止,printtest(2)再去判断if的时候已经不满足条件了,所以就执行System的打印部分,输出n=2,那么printtest(2)中的两部分都已经执行完毕了,出栈
  回到printtest(3),继续执行它里面剩余的System的打印部分,输出n=3,执行完后printtest(3)整个方法执行完毕,出栈
  回到printtest(4),继续执行它里面剩余的System的打印部分,输出n=4,执行完后printtest(3)整个方法执行完毕,出栈
  最后回到main方法,那么main方法里只有调用printttest(4),它执行完毕了,main方法也执行完毕,很自然的整个程序就执行完毕了。最终运行结果如下图控制台部分所示
在这里插入图片描述
  那如果我们对printtest方法中的代码稍微改动一下,用else将System的打印语句包起来,如下所示,那最终结果又会如何呢?
在这里插入图片描述
  根据上面的分析,我们还是画一个类似的图,但是这回每个printtest方法在栈中开辟的空间里,分为if和else两部分,首先还是一样的main方法调用printtest(4),printtest(4)进入if判断,接着调用printtest(3),接着调用printtest(2),按照黑色箭头的顺序逐个调用。
  但是不一样的地方来了,printtest(2)不进入if,而是进入else输出了n=2之后,方法执行完毕,出栈,回到printtest(3),这个时候在printtest(3)方法中,由于if和else的存在,只要执行了if或者是else中的任何一个,该方法就算是执行完毕,所以printtest(3)执行过了if,相当于该方法执行完毕,没有剩余待执行的方法,所以直接出栈,后面的printtest(4)也是一样,所以自然的最终结果只有n=2
在这里插入图片描述
  2)阶乘问题,这个阶乘问题是编程人员入门刚开始学习的一个常见案例,这里就写出来大家根据上面的两个分析自己可以再看一下,熟悉一下递归。

public static void main(String args[]) {
		//给一个标志位:在输入不为整数的时候可以重新输入
		boolean flag = true;
		//输入一个整数
		System.out.println("请输入一个整数:");
		Scanner s = new Scanner(System.in);
		while(flag) {
			String number = s.nextLine();
			//判断是否是整数
			if(isInteger(number)) {
				//递归调用方法计算这个数的阶乘
				int result = factorial(Integer.parseInt(number));
				System.out.println(number+"的阶乘是:"+result);
				//资源关闭
				s.close();
				flag = false;
			}else {
				System.out.println("输入的数不是整数!请重新输入:");
			}
		}
	}
	
	/******************递归调用方法求阶乘******************/
	public static int factorial(int n) {
		if(n == 1) {
			return 1;
		}else {
			return factorial(n-1)*n;	
		}
	}
	
	//写一个方法,用于判断输入的数是否是整数
	public static boolean isInteger(String str) {    
	    Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");    
	    return pattern.matcher(str).matches();    
	}

递归能解决的问题和规则

递归用于解决什么样的问题:
1)各种数学问题:如8皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子问题
2)各种算法中也会用到递归,比如快排,归并排序,二分查找,分治算法等
3)将用栈解决的问题改为用递归解决能让代码更简洁
使用递归的规则:
1)执行一个方法,就创建一个新的受保护的独立空间(栈空间)
2)方法的局部变量是独立的,不会相互影响(比如我们上面打印问题中的每次方法传入的变量n)
3)如果方法中使用的是引用类型的变量,就会共享该引用类型的数据
4)递归必须向递归结束的条件逼近,否则就是无限递归,出现StackOverflowError错误
5)当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕

迷宫回溯问题

迷宫回溯问题分析

迷宫回溯问题,我们首先需要一个地图,很简单我们就用一个二维数组去表示。接着我们需要对二维数组的值进行规定:
如果为0表示地图上这个点没走过
如果为1表示地图上这个点是墙,不能走
如果为2表示地图上这个点走过了,且为通路
如果为3表示地图上这个点走过了,但走不通
地图有了之后,最重要的就是寻路的递归方法:直接先上代码,这里不知道怎么用文字说明,所以把分析都写在注释里了,注释和代码结合起来理解

迷宫回溯问题代码实现

package com.recursion;

/**
 * 用递归解决迷宫回溯问题
 * 		起始位置是int[1][1]
 * 		终点位置是int[6][5]
 * @author centuowang
 *
 */
public class MiGong {
	
	public static void main(String args[]) {
		
		//先用一个二维数组去模拟迷宫地图,用1表示墙
		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;
		
		//打印一下地图
		System.out.println("初始化地图:");
		for(int i = 0; i < 8; i++) {
			for(int j = 0; j <7; j++) {
				System.out.printf("%d\t",map[i][j]);
			}
			System.out.println();
		}
		
		//调用递归函数去寻路
		Pathfinding(map, 1, 1);
		
		//寻路完之后,再打印一下地图
		System.out.println("寻路之后的地图:");
		for(int i = 0; i < 8; i++) {
			for(int j = 0; j <7; j++) {
				System.out.printf("%d\t",map[i][j]);
			}
			System.out.println();
		}
		
	}
	
	/**
	 * @param
	 * 		map:地图
	 * 		i:当前所在行
	 * 		j:当前所在列
	 * @return
	 * 		如果找到返回true,找不到返回false
	 * 约定:当表示地图的二维数组map中值为0表示没走过
	 * 		为1表示墙不能走
	 * 		为2表示走过了,且能走是通路
	 * 		为3表示走过了,但走不通
	 * 规定一个策略:按照下、右、上、左的顺序进行寻路
	 */
	public static boolean Pathfinding(int[][] map, int i, int j) {
		if(map[6][5] == 2) {
			//如果终点位置int[6][5]已经为2,说明已经到了终点
			return true;
		}else {
			//如果还没到终点的话,且如果这个点还没走过的话
			if(map[i][j] == 0) {
				//先将它设为2,暂且表示为通路
				map[i][j] = 2;
				//接着按照策略下、右、上、左进行下一步的寻路
				if(Pathfinding(map, i+1, j)) {
					return true;
				}else if(Pathfinding(map, i, j+1)) {
					return true;
				}else if(Pathfinding(map, i-1, j)) {
					return true;	
				}else if(Pathfinding(map, i, j-1)) {
					return true;
				}else {
					//如果四个方向都走过了都不行,那说明这个点不为通路,置为3
					map[i][j] = 3;
					return false;
				}
			}else {
				//如果这个点已经走过了
				return false;
			}
		}
	}
}

迷宫回溯问题的最短路径

在上面的递归方法中,可以看到寻路规则是程序员自己定的,我们这里规定的是:下右上左。那么我们在没有学更好的算法之前,只能用暴力的方法去寻找,即修改寻路规则,用所有可能的寻路跪规则去寻路,将找出的路径的节点数进行统计,取最小的那条路径即可。当然这个方法很差劲,所以我们这里暂且先把这个最小路径问题放一放,在后面的博客中我们会讲到最短寻路算法相关的内容

八皇后问题

在8×8的国际象棋上摆放八个皇后,使其不能相互攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。问:有多少种摆法?
这个问题在很多的面试和笔试中都会碰到,在学校学数据结构的时候老师也是个会被老师经常挂在嘴边的问题,下面我们就来看看如何去解
在这里插入图片描述

八皇后问题分析

解决八皇后问题主要的关键点有两个:一个解决冲突问题(即解决任意两个皇后不能再同一行、同一列、同一斜线上),另一个就是找出所有可能解
所以这里我们肯定至少需要两个方法:一个用来判断任意两个皇后是否冲突,另一个用来递归实现找出所有可能解
判断是否冲突的方法好写,比较难理解的是如何用递归找出所有可能解,思路如下:

总体思路:
对每个皇后,都将第i个皇后放在第i行。那么先将第一个皇后放在第一行的第一列,找出所有可能解,接着放在第二列,第三列…直到第八列,依次找出所有可能解

具体思路:
1)先把第一个皇后放在第一行的第一列

2)接着开始放后面的皇后,每个皇后都放在上一皇后所在行的下一行,并且从下一行的第一列开始放,判断是否和底下的任一皇后有冲突,如果有的话,就再往右移动一列

3)如果放到某一行发现为死路,放不下去了,那么就开始进行回溯,回退到上一行,判断该行是否已经在最后一列了,如果是的话,继续回退到上一行,不是的话,往右移动一列,判断是否和底下的任一皇后有冲突,如果有的话,就再往右移动一列,如果没有的话就回到(2)步

4)当第一个皇后(也就是第一行的皇后)放在第一列的情况下,八个皇后都摆放好后的第一种情况完成之后,又开始进行回溯

5)回溯的思路:从第八行(也就是第八个皇后)开始,判断他是否在最后一列,如果不是的话,将它所在列往右移动一列,然后继续判断是否和底下的任意一个皇后有冲突,如果没有,又是一种解。

6)如果移到最后一列了都不行,或本身就在最后一列的话,那就回溯到倒数第二行,将倒数第二行的皇后往右移动一列。然后将下一行的皇后从第一列开始重新摆一遍

7)后面依次往下回一行溯,重复上面的(5)(6)步骤

思路图解

先摆好第一个皇后,放在第一行第一列,如下图紫色圆形棋子
接着往上每一行都从第一列开始摆放一个皇后(黑线表示再第一列从左往右开始判断是否和底下的任一皇后有冲突)
那么当第七个皇后放好之后(如下图),就会发现,第八个皇后没有位置可以放,不管放哪里都会冲突,这个时候就需要开始回溯
在这里插入图片描述
回溯就是回退到上一行,从第七行开始判断,是否已经移动到了最右一列,如果是的话(很显然上图中的第七行已经再最右列了),再回退到第六行,继续判断是否已经移动到了最右一列(显然这里没有),那么就将第六行的皇后,往右移动一列,直到找到下一个能与低下的皇后都不冲突位置,如下图(红色线表示回退之后改行重新往右移动的路径)。这里我们又可以发现,第六行移动到新位置之后,我们要开始摆放第七行的皇后了,但此时发现第七行的皇后又没有合适的位置摆放了,所以继续按照上诉规则逐级往下回溯
在这里插入图片描述
那么我们可以找到的第一种摆放方式如下图。找好了第一种摆法(如下图)之后,又开始进行回溯。
在这里插入图片描述
首先往右移动第八行的皇后,发现没有不冲突的位置。那么就回退到第七行,发现第七行往右移动到第四列是可以的(放到下图第七行的黑色位置),放好之后,继续摆放第八行的皇后,发现第八行没有合适的位置可放,继续会退到第七行,再往右发现,第七行也没有合适的位置可放了。
在这里插入图片描述
那就再往下回退到第六行,然后重复上诉判断,逐级回溯,直到找出第一个皇后在第一行一列的所有解后,在将第一个皇后移动到第二行,继续重复上诉所有操作。

上面的文字叙述可能有点晕头转向,下面画了个大概的逻辑思路,可供参考
在这里插入图片描述

八皇后问题代码实现

package com.recursion;

/**
 * 八皇后问题
 * @author centuowang
 * @param
 * 		number:确定皇后的数量
 *		array:存放皇后的位置
 *		total_solution:记录一共有多少种解法
 *		total_frequency:记录一共递归了多少次
 *		total_check:记录一共调用了多少次检查位置的方法
 *
 */
public class EightQueen {
	
	//确定皇后的数量
	int number = 8;
	//用一个一维来存放皇后的位置
	//如果array[] = {0,4,7,5,2,6,1,3};即表示第一个皇后在第一列,第二个皇后在第五列,第三个皇后在第八列...
	int[] array = new int[number];
	//用变量来记录一共有多少种解法,记录计算机一共递归了多少次,记录一共调用了多少次检查位置的方法
	static int total_solution,total_frequency,total_check;
	
	public static void main(String args[]) {
		EightQueen eq = new EightQueen();
		eq.place(0);//从第一行,也就是从第一个皇后开始摆放位置
		System.out.printf("一共有%d种解法\n",total_solution);
		System.out.printf("一共调用了%d次递归\n",total_frequency);
		System.out.printf("一共进行了%d次判断是否位置冲突\n",total_check);
	}
	
	/**
	 * 检查皇后的位置是否合理
	 * @param
	 * 		n:表示检查第几个皇后的位置是否合理
	 */
	private boolean checkLocation(int n) {
		total_check++;//每次一进来检查的次数就+1
		for(int i = 0; i<n; i++) {
			//不用判断行,因为我们已经规定了每一行放一个,所以判断列和斜线即可
			//这里判断是否在统一斜线用了java自带的函数Math.abs
			//Math.abs(n-i) == Math.abs(array[n] - array[i])可以理解为斜线斜率为1,即y2 - y1 = x2 - x1
			if(array[n] == array[i] || Math.abs(n-i) == Math.abs(array[n] - array[i])) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * 递归调用方法去寻找摆放的位置
	 * @param
	 * 		n:表示放置第n个皇后
	 * !!注意:这里的递归是放在了for循环里面,所以这里比较难理解的是for循环里嵌入递归
	 */
	private void place(int n) {
		total_frequency++;//每次一进来递归次数就+1
		if(n == number) {
			//如果n到8了,说明八个皇后已经放好,因为数组下标从0开始,最大到7
			//既然八个皇后都放好了我们就打印一下结果
			print();
			total_solution++;//解法+1
		}else {
			//否则的话,依次放入皇后,并判断位置是否冲突
			for(int i = 0 ;i < number; i++) {
				//先把当前皇后放到对应行的第一列
				array[n] = i;
				//判断位置是否冲突
				if(checkLocation(n)) {
					//如果不冲突,递归继续放下一行
					place(n+1);
				}
				//如果冲突,那么就继续执行for循环,array[n] = i;i自动加一,改行的位置就会自动的往右移一列
			}
		}
	}
	
	/**
	 * 写一个方法,用来打印array数组,方便显示
	 */
	private void print() {
		for(int i = 0; i<array.length; i++) {
			System.out.printf(array[i]+" ");
		}
		System.out.println();
	}
}

写在最后

其实通过上面的代码我们运行之后,会看到如下结果:
在这里插入图片描述
我们发现要得到92种解法,需要调用递归2057次,而要调用判断位置是否冲突更是需要调用一万多次,那么上面代码种的开销是非常大的。如果减少这些开销呢,这里就有人提出了用贪心算法等去优化的问题,这个在我们后面讲到具体的算法的时候,会做详细讲解,这里暂且就抛出这个问题。

下一篇: 从1开始学Java数据结构与算法——八种排序算法讲解分析与代码实现.

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java大魔王

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

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

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

打赏作者

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

抵扣说明:

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

余额充值