一、递归
1.实现递归
去的过程为递,回的过程为归。递归要满足的三个条件:
- 一个问题可以分解为几个子问题的解;
- 问题与子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件
关键:写出递推公式,找到终止条件。
示例:n个台阶,每次可以跨1或2个台阶,请问走这n个台阶有多少种走法?
求解:
递推公式:根据第一步的走法把所有走法分两类:第一类是第一步走了1个台阶,之后的走法就是n-1个台阶的走法;第二类是第一步走了2个台阶,之后的走法就是n-2个台阶的走法,因此递归公式为:
f(n) = f(n-1)+f(n-2)
终止条件:先设置终止条件为f(1) = 1,用比较小的数试验一下:n = 2时,f(2) = f(1) + f(0),不符合逻辑,所以加上f(2) = 2。再拿n = 3,n = 4验证一下是否正确。
因此最后递归代码为(有重复计算的问题):
int f(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}
空间复杂度为
O
(
n
)
O(n)
O(n)。
对于较复杂的递归问题,例如A分为B, C, D,考虑问题时可以假设子问题已经解决,隐藏掉细节。
2. 警惕堆栈溢出
函数调用会使用栈来保存实时变量,而系统栈或虚拟机栈空间一般不大,当递归数据规模大、调用层次深时,可能有堆栈溢出的风险。如果最大深度较小,例如10、50,可以在递归函数中可以增加判断报错的代码。
3. 警惕重复计算
上面的台阶代码有重复计算的问题,可以通过一个数据结构(例如散列表)来保存已经求解过的f(k):
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;
}
递归的应用
DFS深度优先搜索,前中后序二叉树遍历等。
递归的调试
- 打印日志发现,递归值。
- 结合条件断点进行调试。