递归
递归就是求解问题的一种固定编程方式。它的思路就是将问题一层层往外递出去,直到遇到出口后,再一层层向里回归答案。最终得出结果。而每次递出的问题还是用同样的方式解决。
递归需要满足的三个条件
什么样的问题可以用递归来解决呢?
- 一个问题的解可以分解成几个子问题的解,子问题解决了,大问题也就解决了:子问题(数据规模更小的问题)。例如:根据n-1时的解求n的解;分治问题。
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
- 存在递归终止条件:某个数据规模时可以直接得到答案了,不需要继续传递。也就是有了出口。
如何编写递归代码
找到递推关系(例:由n-1推出n),找到终止条件。
例如:有n个台阶,每次可以跨1个台阶或2个台阶,问走完n个台阶有多少种走法?
思路:开始走第一步时有两种走法:1个台阶或两个台阶。那么
n个台阶的总走法 = 以1个台阶开头的剩余n-1个台阶的走法 + 以2个台阶开头的剩余n-2个台阶的走法;
依次递推n-1个台阶的走法和n-2个台阶的走法都可以用上面的公式。
总结公式:f(n) = f(n-1) + f(n-2);
找到了递推公式,再找终止条件:当n=?时,可以直接得到具体数据。很容易想到当n=1时只有一种走法f(1) = 1然而根据?的递推公式,它是由两个条件推出来的,因此n=2时,不好再用递推公式,也要用作出口直接得出数据。
f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2)
复制代码代码就很容易了:
int f(int n) {
if(n==1) return 1;
if(n==2) return 2;
return f(n-1) + f(n-2);
}
复制代码为什么用递归
当数据规模很大时,人脑可能无法一步步循环,一层层往下掉,在一层层返回,计算出问题的结果,但是计算机可以。计算机善于做重复的计算,所以递归正迎合了这点。
因此当思考一个问题A的解时,可以分解成若干子问题B、C、D,可以假设B、C、D都已经解决了,那么如何通过B、C、D的解获得A的解呢?具体的递归细节不用每一步都想清楚。
如?问题,如果我知道了n-1个台阶的走法和n-2个台阶的走法,那很容易就知道n个台阶的走法了。
递归代码要警惕堆栈溢出
根据前面所学的栈的知识,会发现递归函数很容易出现栈溢出的问题,所以当数据规模较大时,递归并不适用。
递归代码要警惕重复计算
有些数据重复计算了,解决的办法可以创建一个map来存储计算过的值。
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 hasSovledList.get(n);
}
int ret = f(n-1) + f(n-2);
hasSovledList.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;
}
复制代码这就是一个到这考虑的过程,由n=1算出n =2的解,再又2->3->4....->n,最终变成了循环的方式求解,和递归也差不多。
如何调试递归代码
- 打印日志发现递归值;
- 结合条件断点进行调试。
递归代码的时间复杂度
递归使用的场景:求解一个大问题a可以分解成求解多个子问题b、c。那么a的执行时间就应该等于子问题执行时间的合并结果。
T(a) = T(b) + T(c) + K // k等于将子问题结果合并的时间
复制代码因此,不仅递归求解问题可以写递推公式,递推代码的时间复杂度也可以写成递推公式。

被折叠的 条评论
为什么被折叠?



