什么是递归
递归是算法中一种非常重要的思想。递归,简单来说就是在函数中存在着调用函数本身的情况。形式是
void f(参数){
f(参数)
}
进一步剖析递归,先有“递”再有“归”。“递”的意思是将问题拆解成子问题来解决,子问题再拆解成子子问题,…,直到被拆解的子问题无需再拆解成更细的问题,也就是拆分成可以求解的问题。“归”就是最小的问题解决了,那么它的上一层问题也就解决了,上一层的子问题解决了,上上层的问题自然也就解决了,…,直到最开始的问题解决。以求解经典的阶层问题为例,f(n)函数用来求解n!。假设求f(5),f(5)=5×f(4),把f(5)拆解成求f(4)这个子问题,然后f(4)=4×f(3),f(3)=3f(2),f(2)=2f(1),f(1)=1。拆解成f(1)可以直接得出结果为1后,f(2)就可以求解,从而f(3),f(4),f(5)就可以逐一求解。
递归算法通用解决思路
上面剖析了什么是递归,可以发现递归有以下两个特点:
1.一个问题可以分解成具有相同解决思路的子问题,子子问题,换句话说这些问题都能调用同一个函数。
2.经过层层分解的子问题最后一定是有一个不能再分解的固定值的(终止条件),如果没有相当于无穷无尽地分解子问题了,问题显然是无解的。
所以解递归题的关键在于我们首先需要根据以上递归的两个特点判断题目是否可以用递归解。基本套路是:
1.定义一个函数,明确这个函数的功能,由于递归的特点是问题和子问题都会调用函数自身,所以函数的功能一旦确定,之后只要找寻问题与子问题的递归关系即可。
2.寻找问题与子问题间的关系(即递推公式),所谓的关系就是可以用公式表示出来,比如f(n)=n×f(n-1),发现递推公式后要找寻最终不可再分解的子问题的解(即临界条件),确保问题的分解是有尽头的。
关于递归思想的学习不是很容易掌握的,所以以下的基础练习题尽管有更简单或者通用的解法,我们都不用,都用递归来解决,来更好的体会递归的思想。
求阶层
public class _求阶层 {
public static void main(String[] args) {
int n = 5;
int result = factorial(n);
System.out.println(result);
}
static int factorial(int n) {
//临界条件
if(n==1)
return 1;
//递推公式
return n*factorial(n-1);
}
}//120
对数组的所有元素求和
对数组中的n个数字求和,用递归的思想可以分解为求第n个数+前n-1个数的和,前n-1个数的和可以分解为第n-1个数+前n-2个数的和,以此类推。该问题重复求解的是一个数与它前面所有数的和相加,直到递推到第一个数,它的和就是本身,这是递归的终止条件。
public class _数组所有元素求和 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9,10};
int len = arr.length;
int result = sum(arr,len-1);
System.out.println(result);
}
private static int sum(int[] arr, int i) {
//终止条件
if(i == 0)
return arr[0];
//递归公式
return arr[i]+sum(arr,i-1);
}
}//55
翻转字符串
翻转字符串就是把字符串的顺序颠倒,“abcde”变为“edcba”。用递归的思想可以认为,如果字符串有n个字符,假设前面n-1个字符已经翻转了,只需把最后一个字符拼接到已翻转的字符串前面。下一层子问题就是把第n-1个字符拼接到前面n-2个已经翻转的字符串前面。以此类推,直到最后一个字符返回。
public class _翻转字符串 {
public static void main(String[] args) {
String str = "abcde";
int len = str.length();
String strReverse = reverse(str,len-1);
System.out.println(strReverse);
}
private static String reverse(String str,int i) {
//终止条件
if(i == 0)
return str.charAt(0)+"";
//递归公式
return str.charAt(i)+reverse(str,i-1);
}
}//edcba
斐波那契数列
斐波那契数列就是1,1,2,3,5,8,13,…其中第一项和第二项都是1,此后的每一项都是它前面两项的和,求第n项是多少。要求第n项,就得知道第n-1项和第n-2项。每一项的求法都是如此,从后面一直递推到前面,直到第二项和第一项为止。
public class _斐波那契数列 {
public static void main(String[] args) {
int n = 6;
int result = fibonacci(n);
System.out.println(result);
}
private static int fibonacci(int n) {
if(n == 1 || n == 2)
return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
}//8
这道题与前面不同,前面的递归都是只有一条路径径直往前推,n推到n-1,n-1推到n-2,…,这道题不能单纯地往前推,它相当于在求n-1的结果时也要考虑到n-2的结果。画图直观一点,
一条路径径直往前推:
分叉递推
最大公约数
利用辗转相除法求最大公约数
public class _最大公约数 {
public static void main(String[] args) {
int m = 27;
int n = 15;
int result = gcb(m,n);
System.out.println(result);
}
private static int gcb(int m, int n) {
if(n == 0)
return m;
return gcb(n,m%n);
}
}
//3
汉诺塔
汉诺塔问题就是有三个柱子A,B,C,在A上有N个盘子,盘子的直径从上到下依次增大,将N个盘子从A移动到C,移动过程中必须保证小盘子在上,大盘子在下,且一次只能移动一个盘子。求移动的方案。
首先我们来看只有两个盘子的情况,把A上的盘子移动到C上:
首先,把第一个盘子移动到B:
然后把第二个盘子移动到C:
最后把B上的盘子移动到C:
这样就完成了把两个盘子从A移动到C。
如果是将n个盘子从A移动到C,我们可以把n-1个盘子看成是一个盘子,这样思路就跟上面的一致了:将n-1个盘子从A移到B,将第n个盘子从A移到C,再把n-1个盘子从B移动到C。
public class _汉诺塔 {
public static void main(String[] args) {
int n= 3;
move(n,"A","B","C");
}
private static void move(int n, String a, String b, String c) {
if(n == 1)
System.out.println("move"+" "+n+" "+"from "+a+" to "+c);
else {
move(n-1,a,c,b);
System.out.println("move"+" "+n+" "+"from "+a+" to "+b);
move(n-1,b,a,c);
}
}
}
青蛙跳楼梯
一只青蛙可以一次跳1级台阶或一次跳2级台阶,例如:跳上第一级台阶只有一种跳法;直接跳1级即可,跳上两级台阶有两种跳法:每次跳一级,跳两次;或者一次跳2级。n级台阶有多少种跳法?
自上而下地思考,如果要跳到n级只能从n-1级跳或者从n-2级跳,所以问题转换为跳上n-1级和跳上n-2级台阶的跳法,若f(n)表示跳上n级台阶的跳法,能够递推出f(n)=f(n-1)+f(n-2)。
public class _青蛙跳台阶 {
public static void main(String[] args) {
int n=5;
int result = f(n);
System.out.println(result);
}
private static int f(int n) {
if(n == 1)
return 1;
if(n == 2)
return 2;
return f(n-1)+f(n-2);
}
}
每次递归调用时,都会在栈里给这个方法分配一个空间,所以递归的调用次数过多的话,会很消耗内存空间。