转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/65537530
什么是递归
简单的说:递归就是程序不断调用自身,递归方法就是方法直接或间接调用自身的方法,递归是一个很有用的程序设计技术!
递归有两大特点:
- 反复执行的过程(调用自身)
- 结束反复执行过程的条件(方法跳出点)
递归缺点:
耗内存,耗资源,不易阅读!不易阅读!不易阅读!(重要的事情说三遍)
递归的设计
一个递归调用可以导致更多的递归调用,因为这个方法继续把每个子问题分解成新的子问题,要终止一个递归方法,问题最后必须达到一个终止条件。当问题达到这个终止条件时,就将结果返回给调用者。然后调用者进行计算并将结果反悔给自己的调用者。这个过程持续进行,知道结果传给原始的调用者位为止。
递归的设计通常包含两部分:
1.递归的定义:将大问题转化为小问题求解。
递归定义就是对问题分解,将一个问题分解为规模较小的问题并用相同的程序去解决。
2.递归的终止条件:跳出递归,返回最小问题的解。
递归终止条件通常就是得出最小问题的解,并返回给他的调用者。
使用递归解决问题
所有的递归程序都具有一下特点:
- 这些方法使用if-else或switch语句来引导不同的情况
- 一个或多个基础情况(最简单的情况)用来终止递归。
- 每次递归都会简化原始问题,让它不断接近基础情况,知道它变成基础情况(最小问题)为止。
通常,要使用递归解决问题,就要将这个问题分解为子问题。每个子问题几乎与原始问题一样的,只是规模小一些。可以应用相同的方法(程序)来递归解决子问题。
n的阶乘
阶乘是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。
n的阶乘求法
n的阶乘常见有两种方法求解:
第一种是循环(最常见)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
输出结果:
3628800
第二种就是递归
根据第一篇文章(顶部有传送门)中提过的递归的两大设计要点来设计:定义和终止条件
1.根据我们要的数据,程序定义就是:
程序返回的是当前n的阶乘的值
重点来了:
假设我们求的是10的阶乘【10!】,那么就必须要知道9的阶乘【9!】,由此得出【10! = 9! * 10】
每个阶层都以此类推,当到达1时,我们都知道:1的阶乘的值为1,那么直接返回1就好了!
用函数f()表示阶乘【n!】:f(n) = f(n-1) * n
2.终止条件
当n到达1时,返回1的阶乘【1! = 1】【return 1】,否则继续调用自身,返回前一个数的阶乘的值
当然,你也可以在n=2时返回2或者在n=4时返回24,只要符合结果就行!(但是这就要知道n的取值范围了)
3.根据上述,不难得出程序代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
斐波那契数列
斐波纳契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……
这个数列从第三项开始,每一项都等于前两项之和。
递归求斐波那契(Fibonacci)数列
在这里我将根据递归两大特点(法则)和斐波那契数列的逻辑来设计对应的递归程序
1.递归程序的定义
首先,设计一个fibo(int n)方法,返回值为:数列中第n个数的值
【注:斐波那契数第一个数值为1,第二个数值也是1,从第三个开始,每个数值为前两个数的和】
当求【n=5】时,实际上是求当【n=3】和【n=4】时的和;
那么求【n=3】就是求【n=1】和【n=2】时的和…..
以此类推….
当【n=1】或【n=2】时,程序直接返回1。
可得出公式:
f(n) = f(n-1) + f(n-2)
不难得出基本的递归调用代码:
- 1
- 2
- 3
细心的人会发现这段代码是会得到递归死循环的,因为缺少了递归终止条件(跳出条件)
2.递归程序的终止条件
1. 当【n=1】或【n=2】时,程序直接返回1。
- 1
- 2
- 3
2. 当然,也可以设计程序当【n=0】时返回0以及【n=1】时返回1;
- 1
- 2
- 3
- 4
- 5
因为【n=2】是由【n=1】和【n=0】的和得到,也是1。
可以得出最终的代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
3. 注意跳出的边界所在
上面代码片中,第一个代码片就限制了n必须大于0,也就是n不能等于0,否则会出现“递归死循环”引发“StackOverflow”异常。
在这里就可以引出“跳出递归调用边界”这种说法,也就是终止递归调用条件的值的设定!这个值的设定是需要根据现实计算情况和要求的计算逻辑实现的。
例如:
在斐波那契数列中,设计终止递归循环的边界是可以随意的,只要符合斐波那契数列的计算逻辑:
1. 终止条件中的n的最小值要大于等于0,小于0没有任何意义,并且不符合斐波那契规则,造成不可估量错误。
2. 要包含当n为基数以及偶数两种情况下n的返回值,主要是因为递归调用时有f(n-1)和f(n-2),那么就一定有奇偶数两种情况。
例子1:可以设计为在n=6时返回8,在n=7时返回13。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
例子2:可以设计为在n=6时返回8,在n=5时返回5。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
两个例子基本相同,就是在调用的时候有不同:
调用时传入参数n的最小值不能小于终止判断条件的最小判断值:
例子1.规定了n必须大于6
例子2.规定了n必须大于5
至于怎么选择,就在于使用者在实际中的需求来调整!
回文串
“回文串”是一个从左读和从右读都一样的字符串,比如“level”或者“noon”等等就是回文串。
常见的回文串有:dad,123454321,123456654321….
回文串判断、定义
- 字符串中的前后两端的字符都是对称的
- 去掉前后两端的字符,剩下的字符串中的字符两端是否相等
- 依次类推,直到剩余的字符串长度为0或1
递归程序的设计思路
一、递归定义
根据上面回文串定义中不难得出设计判断的递归方法的思路:
- 设计一个返回boolean值的方法,如:check(String str);
- 方法参数中传入一个字符串参数,作为需要判断的字符串;
- 将剩下的字符串(子串)作为参数,递归调用自身进行判断
可得出基本的递归代码:【该片段还未加入递归终止条件】
- 1
- 2
- 3
- 4
二、终止条件
- 对该字符串的首字母和末尾字母进行判断:charAt(0)、charAt(str.length() - 1);
如果两端字符串不相等,则直接终止递归,返回false;
- 1
- 2
- 3
- 根据回文串定义,当所有的两端字符都对称相等,并且剩下的字符串长度为0或者1,那么这个字符串才是回文串,不难得出递归程序中的终止条件:
- 1
- 2
- 3
三、最终代码
结合上面的【程序定义】,不难得出最终代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
递归辅助方法
一、递归辅助方法出现的原因
- 看到上面序代码,不少的朋友就会发现,每次的递归调用都是通过【str.substring(int begin, int end)】来获得子字符串并递归调用。
- 大家都知道String在系统中是静态的,substring(int, int)返回的是新的字符串(子串)。
- 当原字符串很长的时候,会产生N多子串,这种做法会消耗非常大的内存空间,而且每次都要进行str.substring(int, int)效率也不高。
二、简单说明递归辅助方法
要解决上面出现的问题可以使用递归辅助方法来处理,
通俗来说,递归辅助方法就是通过方法的重载,给方法设定不同的参数,使在调用的时候通过重载达到我们的需要。
在递归程序设计中定义第二个方法来接收附加的参数是一种常用的设计技巧!
辅助方法在设计数组和字符串问题的递归方案上是非常有用的。
三、使用递归辅助方法求解上面的问题
不难看出,在上面的问题中:
* 都是从需要判断的字符串中获取两端的字符进行对比;
* 并且在每次迭代的过程中下标不断向中间靠拢,最终在剩余1个或0个字符的时候返回true【如果过程中两端字符有不相等的马上返回false】;
1.辅助方法具体设计:
- 我们知道可以通过str.charAt(int)方法来获取对应在字符串下标的字符。
- 那么就可以设计一个递归辅助方法,传入字符串、一个左边下标的整形参数、一个右边下标的整形参数。
- 通过这两个下标参数获取对应的字符串
2.运用重载:
加入一个方法进行重载,这个重载的方法参数只有一个,作为调用上面设计的三个参数方法的入口,主程序调用这个方法即可;
最终代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
在递归程序设计中定义第二个方法来接收附加的参数是一种常用的设计技巧!
辅助方法在设计数组和字符串问题的递归方案上是非常有用的。
题外话:再简单优化
在这两种方案的代码中,都能够发现一个问题:
当字符串长度为1或者0的时候,程序返回true
处理的方法很简单:
在递归调用入口的时候做一个判断检查,就可以解决这个问题!
- 1
- 2
- 3
- 4
深度优先搜索
又称深度搜索、深搜。简单地说深搜就是一种**【不撞南墙不回头】** 的 暴力算法,基本上该算法常用递归作为设计基础,当然也有使用for循环嵌套的,本文是以递归为讲解方向的。
至于更深一层的理论在这里就不详细说明了,详细可以去搜索更多关于。
简单的图搜索问题
本文讲述的是一个基于无向图为基础的图搜索,用二位数组组成的图。
【关于图的更多的理论也麻烦大家去搜索相关的资料,今天写这个文章主要针对下面描述的问题,在这里不过多阐述】
问题描述
描述如下:
在一个n行m列组成的二位数组中,每个单元格代表空地或障碍物。
邻接的单元格距离单位为1,但不包括对角的单元格。
图中是属于无向图,移动的方向不受限制(不能出界)。
现在给定在图中任意的两个坐标(两个均坐标不属于障碍物),求出两个坐标之间到达的最短距离。
如图:
这是一个6行5列的图,其中 (1,2)、(3,2)、(3,3) 、(4,1)、(4,2) 为障碍物
求A点到B点的最短距离
问题分析
问题中可以知道这是一个由二维数组组成的图,每个单元格代表空地或者障碍物。
现在要从A点到达B点或者从B点到达A点,行走的方向可以是(上、下、左、右),同时要避开所有红色的障碍物(如上图)。首先要明白每走一步所到达的位置:
- 当在A点(0,0)时,下一步能到达的点为**(0,1)、(1,0)**
- 当在点**(0,1)时,下一步能到达的点为(0,0)、(1,1)、(1、2)**
- 当在点**(1,0)时,下一步能到达的点为(0,0)、(2,0)、(1、1)**
- 当在点**(1,1)时,由于(1,2)为障碍物**,因此下一步能到达的点为**(0,1)、(1,0)、(2,1)**
- 每一步都去尝试下一步可以到达的位置,直到到达终点B。
有个问题就来了,例如上面的,有的位置是已经被走过的,如果程序没有对走过的位置进行判断,那么可能永远都不能到达B点...
应该怎么做呢?定义一个结果集来记录当前访问过的点。
请慢慢往下看,别急!
递归程序设计思路
一、定义
1. 设定地图
我们可以用一个二维的 int 数组来表示该图,我们假设在数组中:
- 值为 1 的是障碍物
- 值为 0 的是为空地
- 注意:使用二维int数组的原因是:如果需要,可以用数字表示不同类型的障碍物,本问题中可以用boolean数组表示空地或障碍物,但为了让大家更加清晰不和下面的 boolean[][] used 二维标记数组弄混,还是使用 int[][] map 来定义。
那么就可以得出下列二维数组:
0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
0 0 1 1 0
0 1 1 0 0
0 0 0 0 0
2. 首先是定义需要用上的变量:
- int n,m:定义图的大小。
- int[][] map:需要搜寻的图(在这里用int[][]二维数组表示)
- boolean[][] used:大小和图一样,用于标记被访问过的点(访问过为true),保证每次走的都是没有被走过的点,这也是解决上面的重复访问同一个点的问题的方案
- int p,q:终点的Y轴、X轴坐标,由用户输入。
- int count:计算由起点到终点所有的可行路径。
- int minStep:记录最短路径所需要的步数,因为要考虑起点和终点为同一个点,因此设定初始值为 -1。
3.设定一个 dfsMap(...) 方法,该方法主要用于深度搜索图。
除了上面设定的变量,dfsMap(...) 需要管理的参数有:
- int x:当前点所在的X轴坐标值
- int y:当前点所在的Y轴坐标值
- int step:当前点与开始点的距离
二、代码编写的思路
当我们在一个点时,需要做的是要判断当前所在的点是否为终点,如果是终点,那么就对历史记录进行判断,代码如下:
if (y == p && x == q) {
System.out.println("找到一条路径,距离为:" + step);
count++;
if (minStep == -1) {
minStep = step;
}
if (step < minStep) {
minStep = step;
}
}
如果不是终点,那么程序就要去寻找当前点的下一步;同时,我们需要用上boolean[][] used二维数组,大小和当前的地图一样,用于标记当前地图中哪些点被访问过,如果被访问过,那么就跳过该点。
假设目前所在点的位置为 (x,y),那么可以得出下一步可到达的点为:
(x+1, y)、(x-1, y)、(x, y+1)、(x, y-1),如下图:
转换成代码形式就是,该数组可以定义为全局静态变量:
int[][] wayPoint = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
根据上面四个点,用表格来表示(X,Y)坐标的变化量更为直观:(以行为变化单位)
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
用循环就能得到以(X,Y)为中心的周边四个点。
但得出这些点并不能一下子就进行递归寻路操作,要确定这些点是不是能够“走”得到,需要对其进行边界和障碍物以及该点是否被访问过判断。代码如下:
for (int i = 0; i < 4; i++) {
int gX, gY;//新的坐标位置
gX = x + wayPoint[i][0];//获取每行的第0列,即上面表格中X的变化值
gY = y + wayPoint[i][1];//获取每行的第1列,即上面表格中Y 的变化值
//判断越界
if (gX < 0 || gX >= m || gY < 0 || gY >= n) {
continue;
}
//判断障碍物,以及该点是否被访问过
if (map[gY][gX] == 1 || used[gY][gX] == true) {
continue;
}
....
}
三、得到dfs(...)递归体代码
在对新的点进行判断后,就确定该点是能到达的,那么就可以 将当前的结果集(即当前深度搜索所走过的位置的集合) 进行递归,继续交给 dfsMap(...) 方法进行迭代寻找。
在进入该点之前,我们需要标记该点已经被访问过,同时在递归结束之后要对标记进行消除, dsfMap(int, int ,int) 核心代码如下:
/**
* @param x 当前所处的X轴坐标
* @param y 当前所处的Y轴坐标
* @param step 距离
*/
static void dfsMap(int x, int y, int step) {
if (y == p && x == q) {
//System.out.println("找到一条路径,距离为:" + step);
count++;
if (minStep == -1) {
minStep = step;
}
if (step < minStep) {
minStep = step;
}
} else {
for (int i = 0; i < 4; i++) {
int gX, gY;//新的坐标位置
gX = x + wayPoint[i][0];//获取每行的第0列,即上面表格中X的变化值
gY = y + wayPoint[i][1];//获取每行的第1列,即上面表格中Y 的变化值
//判断越界
if (gX < 0 || gX >= m || gY < 0 || gY >= n) {
continue;
}
//判断障碍物,以及该点是否被访问过
if (map[gY][gX] == 1 || used[gY][gX] == true) {
continue;
}
used[gY][gX] = true;
dfsMap(gX, gY, step + 1);
used[gY][gX] = false;
}
}
}
测试
最终完整的代码如下:
//此文老猫原创,转载请加本文连接:
//https://i-blog.csdnimg.cn/blog_migrate/06ba7cb058766f22c320f51088a9eda4.png
//更多有关老猫的文章:http://blog.csdn.net/nthack5730
public class SearchMap {
static int[][] map;
static boolean[][] used;
//图面积设置
static int n;
static int m;
//需要寻找的点
static int p;
static int q;
//最小位置
static int minStep = -1;
//次数统计
static int count = 0;
static int[][] wayPoint = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("输入图的行、列:");
n =scan.nextInt();
m =scan.nextInt();
System.out.println("输入开始点的坐标:");
int startX = scan.nextInt();
int startY = scan.nextInt();
System.out.println("输入终点的坐标:");
p =scan.nextInt();
q =scan.nextInt();
map = new int[n][m];
used = new boolean[n][m];
System.out.println("输入图数据:");
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
map[i][j] = scan.nextInt();
}
}
//递归调用开始
used[startY][startX] = true;//初始化开始点被访问过,注意Y值代表行,X值代表列
dfsMap(startX, startY, 0);
//输出结果
System.out.println("\n//=============================");
System.out.println("// 找到的总路径数为:" + count);
if (minStep == -1) {
System.out.println("// 没有找到结果");
} else {
System.out.println("// 最小距离为:" + minStep);
}
System.out.println("\n//=============================");
}
/**
* 深搜暴力寻图
*
* @param x 当前所处的X轴坐标
* @param y 当前所处的Y轴坐标
* @param step 距离
*/
static void dfsMap(int x, int y, int step) {
if (y == p && x == q) {
// System.out.println("找到一条路径,距离为:" + step);
count++;
if (minStep == -1) {
minStep = step;
}
if (step < minStep) {
minStep = step;
}
} else {
for (int i = 0; i < 4; i++) {
int gX, gY;//新的坐标位置
gX = x + wayPoint[i][0];//获取每行的第0列,即上面表格中X的变化值
gY = y + wayPoint[i][1];//获取每行的第1列,即上面表格中Y 的变化值
//判断越界
if (gX < 0 || gX >= m || gY < 0 || gY >= n) {
continue;
}
//判断障碍物,以及该点是否被访问过
if (map[gY][gX] == 1 || used[gY][gX] == true) {
continue;
}
used[gY][gX] = true;
dfsMap(gX, gY, step + 1);
used[gY][gX] = false;
}
}
}
}
测试输入如下:
输入图的行、列:
6 5
输入开始点的坐标:
0 0
输入终点的坐标:
4 3
输入图数据:
0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
0 0 1 1 0
0 1 1 0 0
0 0 0 0 0
程序输出:
//=============================
// 找到的总路径数为:124
// 最小距离为:9
//=============================
总结
难点所在
至此,深度搜索图的最短路径已经完成。其中最难理解的应该就是每次递归前标记位置已经被访问,并且在递归结束(相当于当前层)后对标记进行撤销:
....
used[gY][gX] = true;
dfsMap(gX, gY, step + 1);
used[gY][gX] = false;
....
回到图搜索
我们将图分为两类结果集:
- 一类是“已经被访问过的”结果集
- 剩下的就是“没有被访问过的”结果集
- 每次进行下一步都是以 当前“已经被访问过的”集合 为基础,将当前的结果集继续迭代
- 当 “当前的结果集” 所有的可能性都被尝试完时,就要将当前结果集的最后一步还原为上一个结果集的状态。
在这里,我简单地用数学集合表示法描述下:
按照上图,假设程序在前面访问了2个点:(0,0)、(0,1),其中(0,1)是目前游标所在(最后一个访问的)。
设U为全图所有点的集合,设A为“已经被访问过的”结果集,当**A ={(0,0)、(0,1)}**时,剩下的 {U - A} 都是没有被访问的集合。
按照【每次只能走一步】的约定,当我们要走下一步时只能走(0,2)、(1,1)两个中的一个:
- 按照顺时针访问顺序,我们先走(0,2)这个点,对应代码中标记:
used[gY][gX] = true;
- 当走到(0,2)时,集合A就变为{(0,0),(0,1),(0,2)},设为A1,如果还要继续往下走,那么就要在A1的基础上继续扩展,对应代码中递归调用,表示继续从(0,2)这个点继续扩展其所有的结果:
dfsMap(gX, gY, step + 1);
- 当A1所有的情况都尝试完的时候,A1就要返回A的集合状态,这时就要从集合A1中移除(0,2),在代码中也就是取消(0,2)的标记:
used[gY][gX] = false;
- 当返回到集合A的数据时,就要去访问(1,1)这个点,继续重复上面的1,2,3步。
至于扩展的顺序,就是根据上面定义的方向数组waypoint数组,用for循环获取所有的(上、下、左、右)可能,然后进行1,2,3步
当然,在进行递归迭代之前,要对新的点进行边界、障碍物判断。
这个过程与全排列生成的解答树相似
图片参考《算法竞赛:入门经典》中P119页的图。
里面通过描述全排列生成的解答树,和本题的思维非常相似,如图:
和全排列相似地,整个过程就如同生成一棵解答树【如图】:
- 每到达一个结点,所有已知的(走过的)都是一个结果集;
- 同时当前结果集与下一个可行的结点又会形成一个新的结果集(可行的结点越多,新的结果集越多);
- 如此下去,直到当前结果集的所有可行结点被列举,返回当前结果集的上一个结果集;
- 当所有的结果集都被列举,那么就能得出所有可行性的遍历。