什么是递归?(what)
- 递归时一种应用广泛的算法(或者编程技巧),很多算法的编码实现都要用到递归,比如DFS深度优先搜索、前中后序二叉树遍历等;
- 所有的递归问题都可以用递推公式来表达:f(n)=f(n-1)+a,f(1)=b;
-递归就是函数调用函数本身,函数有返回值,并在一定条件下终止调用一层层返回。 - 递归,函数调用自身:递 ,函数有返回值:归;
为什么用递归?(why)
优点:代码表达力强,简洁;
缺点:空间复杂度高,有堆栈溢出风险,存在重复计算可能,以及过多的函数调用会比较耗时;
什么样的问题用递归?(when)
- 一个问题的解可以分解为几个子问题的解;
例如 f(n)=f(n-1)+f(n-2); - 这个问题与分解的子问题,除数据规模不同,求解思路完全一样;
例如 f(n)=f(n-1)+f(n-2); n 、n-1 、 n-2只是数据不同但求解函数一样都是f - 存在递归终止条件;
例如 f(1)=1;
怎样编写递归代码?
理解
写出递推公式,找到终止条件
递归其实就是一个方程式:f(n) = f(n-1) + a;也就是说在设计递归的时候应该考虑下面三个方面:
- 求解f(n)的时候,假设f(n-1)已经求解出来了。我们不要去考虑f(n-1)是如何求解出来的。
- 关键点在于找到递归的终止条件。
- 递归往往和分治法是分不开的。对于复杂的递归,往往将递归拆分,然后再合并。
步骤
写一个递归方法。
- 首先写判断递归结束时候的操作;
- 再写递归分解操作;
栗子
汉诺塔问题就是使用典型的递归思想。
先推导最简单的f(2),从而推f(n)的解可以分为:
- 将 n-1 个圆盘从 from -> buffer
- 将 1 个圆盘从 from -> to
- 将 n-1 个圆盘从 buffer -> to
- 以上三步都是为了求解f(n),最后我们给出递归结束的条件。只有一个圆盘的时候,只需一次移动操作即可from -> to。
/**
* 汉诺塔问题
*/
public static void move(int n,String from,String buffer,String to){
if(n==1){
System.out.println(from+"—>"+to);
//必须有return
return;
}
move(n-1,from,to,buffer);
move(1,from,buffer,to);
move(n-1,buffer,from,to);
}
电影院问座位第几排问题,f(n)=f(n-1)+1,f(1)=1,
/**
* 电影院座位排数问题
* @param n 前面一排
* @return
*/
public static int ask(int n){
if(n==1){
return 1;
}
return ask(n-1)+1;
}
注意点
1.警惕堆栈溢出:
递归用的是系统栈或者虚拟机函数调用栈,栈空间一般都不大,
如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。
可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。
2.警惕重复计算:
例如 f(n)=f(n-1)+f(n-2) , f(5)=(f4)+f(3),f(4)=f(3)+f(2),这里f(3)重复计算
通过散列表来保存已经求解过的值,从而避免重复计算。
熄灯
- 递归必须有函数调用函数自身;
- 递归必须有return;
- 写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
- 编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。