简单的递归运算
有一个数组 a[5] = {10, 5, 4, 2, 1} ,利用递归运算来求和 sum
public int summation(int i) {
if(i == 0)
return a[0];
return a[i] + summation(i-1);
}
递归运算可以拆分为:
递归运算总是将一个问题的规模不断减小,也可以说递归运算将一个问题拆分为规模不同的相同问题。
递归有个显著特点,会进行自己调用自己的函数,问题规模的减小主要通过函数的参数来实现。
递归和循环
递归和循环都是迭代运算,循环都可以转换为递归问题(比如数组求和),但是有的递归却不能使用循环来解决。
递归运算相比于循环而言,虽然简化了逻辑,但是程序的开销增加,所以,简单的迭代运算,我们如果能使用循环解决,尽量不要使用递归。(递归会调用函数,占用栈空间)
涉及到问题规模不断减小的迭代运算,我们优先选择递归而不是循环。
递归运算的两条法则:
- 有基准情形(即递归结束的标志,递归不能无休止地推进下去)
- 不断推进(递归总是能想着基准情形不断推进)
Tips: 迭代运算最忌讳的就是没有终止条件,基准情形可以说就是递归的终止条件
使用递归解决问题
这里,我们举几个例子,斐波那契(fei bo na qi)数列:
明白人都看得出来,斐波那契数列给出了 基准情形,问题的规模也是不断减小,必须得拿出递归来算啊。
基准情形:
F(1) = 1;
F(2) = 1;
递推公式:
F(n) = F(n -1) + F(n-2)
然后我们就能根据这些信息,写出求斐波那契数列的方法:
public int getFibonacci(int n) {
if(n == 1 || n == 2)
return 1;
else
return getFibonacci(n-1) + getFibonacci(n-2);
}
下面是一个经典的递归例子,汉诺塔问题:
例如:
汉诺塔移动的思路,假设有 A B C 三个柱,目标是 B 柱:
- 将A中的 n - 1 个圆盘移动至 C 柱
- 将A中剩下的第 n 个圆盘移动至 B 柱
- 将 C 柱中的 n - 1 个圆盘移动至 B 柱
当走到步骤 3 的时候,又会从 步骤 1 开始执行,这样我们就可以发现如何来实行递归
其中的基准情形为:当柱中只有一个圆盘,直接移动至 目的柱即可。
图1 a to b; a to c | 图2 b to c | 图3 a to b |
图4 c to a | 图5 c to b | 图6 a to b |
弄清楚汉诺塔的递归过程,我们就可以尝试写出它的代码了(通过输出语句来打印圆盘的移动过程):
public class Test {
public static int count;
/*
* @param n 柱中圆盘个数
* @param A 代表移动原点的柱
* @param goal 代表将要到达的柱中
* @param B 辅助移动的柱子
*/
public static void setHanoi(int n, char A, char goal, char C) {
count++;
if(n == 1) {
System.out.println(A + " to " + goal);
}else{
setHanoi(n - 1, A , C, goal);
System.out.println(A + " to " + goal);
setHanoi(n - 1, C, goal, A);
}
}
public static void main(String[] args) {
Test.setHanoi(3, 'a', 'b', 'c');
System.out.println(count);
}
}/*output:
a to b
a to c
b to c
a to b
c to a
c to b
a to b
7*/
如果,你对汉诺塔的递归问题有所疑问,可以下载一个 汉诺塔3D 游戏辅助理解,同时也可以在游戏中检验我们代码的正确性。
在分析中我们可能遇到的问题:setHanoi()中的参数哪些是变化的哪些是不变的,这里主要是通过参数顺序的改变,来调换 出发点的柱子 和 目的地的柱子,代码中的 goal 是变化的,而出发点也是变化的。
Tips: 使用递推时,要搞清楚两个东西,基准情形和递推规律;在刚开始不要想着递归算法中的递推会进行很多很多次,我们可以用规模较小的输入来验证自己的思路。
分析递推深度
在学习递归思想的时候,我们最迷惑的可能就是递推中的每一层是如何进行的,以及其他语句在递归中执行的先后顺序:
- 在函数调用前的语句在递归前,从外层逐渐深入执行
- 在函数调用后的语句在递推完成后,从深层向外执行
比如之前汉诺塔的递归运算,我们来看看它的深度变化:
public class Test {
public static int count;
/*
* @param n 柱中圆盘个数
* @param A 代表移动原点的柱
* @param goal 代表将要到达的柱中
* @param B 辅助移动的柱子
*/
public static void setHanoi(int n, char A, char goal, char C) {
count++;
if(n == 1) {
System.out.println(getDepth(n)+A + " to " + goal);
}else{
setHanoi(n - 1, A , C, goal);
System.out.println(getDepth(n)+A + " to " + goal);
setHanoi(n - 1, C, goal, A);
}
}
public static String getDepth(int n) {
StringBuilder str = new StringBuilder();
for(int i = 0; i < n; i++) {
str.append("--");
}
return str.toString();
}
public static void main(String[] args) {
Test.setHanoi(64, 'a', 'b', 'c');
System.out.println(count);
}
}
如果用 -
的个数代表圆盘的大小,你会发现递归的是多么适用于汉诺塔,我们通过递归的深度解决了汉诺塔中圆盘大小规则(大圆盘不能放在小圆盘上)。
这个例子貌似并不能明显看出深度的变化,我们在举一个 求阶乘的 例子:
编写代码:
package dg;
public class Test {
public static int factorial(int n) {
int f;
if(n == 1)
return 1;
else {
System.out.println(getDepth(n)+n);
f = n * factorial(n-1);
System.out.println(getDepth(n)+n);
return f;
}
}
public static String getDepth(int n) {
StringBuilder str = new StringBuilder();
for(int i = 0; i < n; i++) {
str.append("--");
}
return str.toString();
}
public static void main(String[] args) {
Test.factorial(5);
}
}
通过这些,我们明显观察到,递归中函数调用前后语句的执行顺序和深度。