如何使用递归来解决问题?

简单的递归运算

有一个数组 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 柱:

  1. 将A中的 n - 1 个圆盘移动至 C 柱
  2. 将A中剩下的第 n 个圆盘移动至 B 柱
  3. 将 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: 使用递推时,要搞清楚两个东西,基准情形和递推规律;在刚开始不要想着递归算法中的递推会进行很多很多次,我们可以用规模较小的输入来验证自己的思路。

分析递推深度

在学习递归思想的时候,我们最迷惑的可能就是递推中的每一层是如何进行的,以及其他语句在递归中执行的先后顺序:

  1. 在函数调用前的语句在递归前,从外层逐渐深入执行
  2. 在函数调用后的语句在递推完成后,从深层向外执行

比如之前汉诺塔的递归运算,我们来看看它的深度变化:

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);
	}
}

在这里插入图片描述
通过这些,我们明显观察到,递归中函数调用前后语句的执行顺序和深度。
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章