递归概述
百度百科: 程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回(即回溯)。 简单来说: 递归就是一种编程技巧,一般出现在一个函数体中,他最主要的特征就是这个函数会在执行过程中会调用自己,就是自己调用自己,本质也是一种循环。
借一个阶乘的例子来理解
如实现n的阶乘 阶乘:n的阶乘=n*(n-1)*(n-2)*…*1 阶乘的实现: 要理解递归,就要明白递归一般有两个步骤,即递归和回溯 递归的过程就是把参与递归的函数先全部展开运行的过程 回溯的过程就是将递归产生的结果进行处理 递归过程中也可能会出现回溯,回溯过程中也会出现递归。(这里后续的迷宫回溯和八皇后会有涉及到)
比如上面的阶乘,假设n为3,函数开始运行的时候,首先他会持续递归, 首先变成3*Factorial(2),然后继续递归,Factorial(2)变成2*Factorial(1),然后继续递归,Factorial(1)变成1,此时,只剩下最外层的一个函数,递归过程结束,此函数的值此时由3*Factorial(2)变成了3*2*1,然后开始回溯,即,将这些函数的值进行最后的操作,这里即乘在一起得出最后的结果
递归实现找到迷宫出口
这里我们创建一个二维数组来表示迷宫地图 然后对迷宫初始化 设置墙壁,将点值设置为1表示墙壁 设置可走的路,将点值设置0表示此点可以行走 这里搞一个8*7的地图,简单设置两个路障 这里把地图设置简单点是为了方便理解和设置,不要觉得这个程序垃圾,这玩意,无论多难的地图,都能给解出来,此处我们是来学习他的核心思想的,不是来解地图的,这只要搞会,啥地图都没问题。只是可能费时间(小声哔哔) 设立规则: 设置起始点,在起始点的基础上对其下标(索引)进行加减,以此来当做点的前后左右移动 走过的点用2标记,未走过的用0标记,死点(无法向0点移动的点)用3标记,、 设置每次移动方式的优先级为:下>右>上>左。即每次先尝试级别高的移动方式,若无法移动再尝试级别低的 设置当点到达某一位置的时候算到达出口,此处简单的设置为map[4][5]点
实现
- 首先得到刚才的地图
- 然后单独写一个类,此类用于实现迷宫问题和地图的打印
- 地图打印
- 然后开始实现找出口的函数,此函数接收三个参数,当前点的位置和代表地图的二维数组
- 然后最难的地方来了,为什么难?因为找出口这部分代码的东西实在太不好理解。代码就那么点,要是理解了,一切就非常简单。我们先来分析整体的设计思路
这里,我们要找到迷宫的出口,就得借助递归的特性。这里也是一种巧妙的递归,此函数体中的一切,都是为了进行递归而巧妙设计的 思路: 在寻找出口的过程中,我们先尝试一直向下移动,若撞到墙壁则往右,然后若再次撞到墙壁则往上,然后若再撞到墙壁则再往左。每次移动结束后,我们把此时所在的点标记为2,我们设置,每次不可以向标记为2的地方移动。若到达某点后,其上下左右都无法移动,则把此点标记为3,然后退回到上一个点(此处是退回,不是移动,所以不用在意不能向标记为2地点移动的规则),在上一个点的基础上再进行移动。我们设置,也不可以向标记为3的地点移动。每次移动结束,并标记当前点为2结束后,都判断是否找到出口,若找到出口,则结束寻找,若未找到,则继续移动。 代码的理解: 知道这些思路后,我们结合思路来看代码。 首先我们看点在迷宫中的移动方式,这里用递归来模拟移动 这里,每次递归就进行一次移动,每次点的移动即在当前点的索引上把某个坐标值减1或加1。移动后的坐标直接作为新的参数传入下次递归的函数体中,这里四个if语句,表示四种移动方式,if结构体中的return true用于回溯的时候,当找到出口,则进入到这里面 首先,每进入一次递归,便判断上一次移动的点是否是出口,即判断map[4][5]是否为2,这是找到出口的判决条件 然后后面,未找到出口,则先判断此时点所处的位置是否是可以到达的位置: 这里只看红线部分就行。用一个if语句来判断,当此时所处点状态不为0,则表示此点不能走,则进入else语句,返回flase,便退出此次递归,回到上一次递归。 - 至此其实最重要的找寻出口的部分已经结束,这个代码精妙之处就在于他拆开看,啥都看不懂,但合在一起,真的是妙蛙种子吃着妙脆角妙进了米奇妙妙屋,秒到家了!
这里也是分为递归和回溯两部分,首先,点进行移动,每次移动都是一次递归,先朝着一个方向使劲递归,像极了不撞南墙不回头的我们,直到撞到墙,知道疼后,才懂得退后,正好退后的操作,就是最后返回false的那一部分,这里回溯到上一次递归,这里有着四次的if-else结构体,四种选择,一个方向不行,换一个方向来横冲直撞,再次遇到痛,这次我们运气好,找到了出口,然后返回true,正好让此条if-else语句执行,然后呢,他也返回true,然后就跟多米诺骨牌,哗!一下,全部返回true,这就是瞬间的回溯,一下子退出了所有的递归,就找到出口了。
全部代码
import java.util.Map;
public class mazeDemo {
public static void main(String[] args) {
int[][] map=new int[8][7];
for (int i = 0; i < 8; i++) {
map[i][0]=1;
map[i][6]=1;
if (i<7){
map[0][i]=1;
map[7][i]=1;
}
}
map[5][5]=1;
map[5][1]=1;
Maze maze=new Maze();
maze.showArray(map);
maze.setWay(map,1,1);
maze.showArray(map);
}
}
class Maze{
public boolean setWay(int[][] map,int i,int j){
if (map[4][5]==2){
System.out.println("已找到出口");
return true;
}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 {
return false;
}
}
}
public void showArray(int [][] map){
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}
}
八皇后
概述
问题表述为:在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 说是八皇后,但我们这里只要你电脑配置够,我们可以实现n皇后 找个图: 源自百度
实现
咱先把8行的棋盘,从下往上,分别是第一行到第八行,从左到右是第一列到第八列。首先,咱知道,一行中是肯定不能摆放大于一个的皇后了。 思路: 把棋盘上每一种摆放方式都试一遍 大致思路: 先找到每行棋子最靠左的可行的摆放方式,然后在依次更改后面行的摆放位置,先从第8行开始,从左到右一格一格的试,试到最右边,然后更换第7行的摆放位置,将其向后移动一位,然后再把第8行的棋子从1列到8列一格一格的试。第8行试完,再第七行再向后移动一位,再低8行从1列到8列的试。直到7行移动到最右边,然后移动第6行的,重复上述操作,直到第一行的试完。至此便能找到所有的结果。 进一步的思路: 编写一个方法体,用于判断所有皇后是否冲突。每次移动一次棋子,都进行一次判断,设置一个变量当做计数器,不冲突则让自变量自增1。 编写判断皇后冲突方法的思路: 邓小平爷爷说过,黑猫白猫,抓到老鼠的就是好猫。 我们这里为了方便,不再使用二维数组来替代棋盘,我们用一个一维数组来替代棋盘。 此一维数组的索引代表棋盘的第几行,索引对应的值,即checkerboard[索引]代表第几列。 然后这样,来判断是否冲突就简单多了,后续对棋子的移动也很简单。 来看方法体的具体实现: 看注释讲解。我太累了,不想再整理一遍了 然后我们还要准备一个打印棋盘的方法 为了简单,此处直接打印一维数组,不将其转换为二维数组再打印了 然后就是核心部分,此处思路: 利用递归的特性,我们每摆放一行就是一次递归,首先找到第一种摆放方式,此时是递归了8次,然后开始回溯。在回溯过程中,用for循环来表示棋子在一行上从1到8的运动测试。 如何判断某次递归是否是第八行摆放皇后? 递归的方法体接收一个整形参数,我们第一次自己调用此方法体我们传入一个初始值,0,然后在递归的时候,每次递归我们都让这个值加一然后传入这个方法体,同时每次递归都判断此值是否到达了8,如果为8则表示,最后一行也已经摆放完成。 核心代码部分 就这么点,show是我们编写的打印棋盘的方法,judge是我们判断当前皇后和前面皇后是否冲突的方法。这个是我去掉注释的版本,后面放出一个有注释的
这里面的if(n=max)用于判断是否完成一次摆放,是的话,把计数器count自增1。然后跳出此次递归,回溯到上一次,继续向后寻找下一种可能。 若不是一种可行的方式,后面一个for循环,把当前棋子向后移动一位(移动操作即checkerboard[n]=i),然后进入下一次递归,在下一次递归中正好判断是否完成一次摆放。如此,一直递归。这里的for选混非常巧妙,他承担着在回溯过程中最重要的角色。第一次摆放方式找到后,此时方法体中会有8个未完成的for循环,这里的回溯,主要的就是执行这些for循环,每次for循环中的每次执行,都又会开创一次递归,每次的递归又会出现回溯,至此,疯狂的扩张,线性增长,便能触及到所有的结果。触及到所有的结果后,正好我们有一个判断if(n=max) 卡着他,让他不至于扩张的太过分,然后所有的扩张结果又会全部回到原点,至此得到全部结果。
正好按照上面的思路来,正好得到第一种摆放方式的时候,正好n等于8,此时便开始回溯。这里有一个小细节:我们的n是从0开始的,当n等于8的时候n是自增了9次,但是按道理我们是只需要递归8此啊?下面解释:此方法体中对n的涉及顺序是:先判断(判断是否到我们想要的结果),然后再自增(即递归传值)。所以当我们n等于7,此时是第8次的递归(即最后一行的摆放),然后按照方法体的设计,此时会再次进入一次递归,此时n便变成8了,正好符合我们的判决条件,然后退出这次的方法体,下面的递归便不会再执行,所以总的来说还是正好把8行给摆放完成。
带注释的全部代码
一定要多看注释!!!
public class EightQueensDemo {
public static void main(String[] args) {
EightQueens queens=new EightQueens(8);
queens.put(0);
System.out.println("共有"+queens.getCount()+"摆放方式");
}
}
class EightQueens{
private int count=0;
private int max;
private int[] checkerboard;
public EightQueens(int max) {
this.max = max;
checkerboard=new int[max];
}
private boolean judge(int n){
for (int i = 0; i < n; i++) {
if (checkerboard[i]==checkerboard[n]||Math.abs(n-i)==Math.abs(checkerboard[n]-checkerboard[i])){
return false;
}
}
return true;
}
public int getCount() {
return count;
}
public void put(int n){
if (n==max){
count++;
show();
return;
}
for (int i = 0; i < max; i++) {
checkerboard[n]=i;
if(judge(n)){
put(n+1);
}else {
continue;
}
}
}
private void show(){
System.out.print("一种摆放方式是:");
for (int i = 0; i < checkerboard.length; i++) {
System.out.print(checkerboard[i]+" ");
}
System.out.println();
}
}
|