如何理解递归
递归是一种应用非常广泛色算法。DFS深度优先搜索、前中后序二叉树遍历都会用到递归。递归对后续复杂算法的理解有很大帮助。
递归需要满族三个条件
- 一个问题的解可以分解成为几个子问题的解
- 这个问题与分解后的子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件
如何编写递归代码
关键两点:
写出递归公式
找到终止条件
递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码
例子:假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?
第一步有两种走法 1.走一步 2.走两步,
递归公式f(n) = f(n-1) + f(n-2)
终止条件,当只有一个台阶,只有走一步这一种走法,当只有两个台阶,可以分两步走,或者直接两步走完
终止条件f(1) = 1; f(2) = 2;
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}
编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。
递归代码要警惕堆栈溢出
递归求解的规模很大,不停的压栈,就会StackOverflow
怎么避免StackOverflow?
设置递归深度超过 k就报错结束递归
// 全局变量,表示递归的深度。
int depth = 0;
int f(int n) {
++depth;
if (depth > 1000) throw exception;
if (n == 1) return 1;
return f(n-1) + 1;
}
但是这个方法并不能完全解决问题,因为最大允许的递归深度跟当前线程剩余的栈空间大小有关
递归代码警惕重复计算
这里f(3)被重复计算,怎么避免这个问题???
通过散列表来保存已经求结果的f(k),当遇到f(3)先检查散列表是否计算过
public int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
// hasSolvedList可以理解成一个Map,key是n,value是f(n)
if (hasSolvedList.containsKey(n)) {
return hasSolvedList.get(n);
}
int ret = f(n-1) + f(n-2);
hasSolvedList.put(n, ret);
return ret;
}
怎么将递归代码改为非递归代码
递归代码有利有弊
利 表达能力强,写起来非常简洁
弊 空间复杂度高,有堆栈溢出风险,存在重复计算,过多函数调用耗时多等问题
走楼梯例子,非递归实现
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
int ret = 0;
int pre = 2;
int prepre = 1;
for (int i = 3; i <= n; ++i) {
ret = pre + prepre;
prepre = pre;
pre = ret;
}
return ret;
}
是不是所有递归代码都可以改成非递归?
理论上是,可以手动模拟函数压栈实现,但是效率不一定提高,因为本质一样的
参考
https://www.cnblogs.com/bakari/p/5349383.html