递归
递归是一种常用的算法(或者是编程技巧),利用递归求解问题的时候,去的过程(一直向下向深处)叫做“递”,回来的过程(由下级深处运算携带结果返回过程)叫“归”。
时间复杂度:递归代码中多了很多函数调用,当函数调用数量较大时,时间成本还是挺高的
空间复杂度:每次递归调用都会在栈中保留一次临时变量,所以空间复杂度通常并不是O(1),可能是O(n)
递归需要满足的三个条件
- 一个问题的解可以分解成几个子问题的解
- 这个问题和分解之后的子问题,除了数据规模不同外,求解思路完全一样
- 有递归终止条件
编写递归代码
关键:写递归公式,找到终止条件,把问题抽象成一个递归公式,不用想一层层的调用关系,不用试图分解递归每个步骤
写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。
假如问题A可以分解为B、C、D,在写的时候可以站在B、C、D已经被解决了的角度去思考,而且,考虑关系的时候只需要考虑问题和子问题之间的关系,不用一层一层往下捋清关系(如考虑:子问题和子子问题的关系,子子问题和子子子问题的关系),屏蔽掉递归细节。
需要注意问题
可能导致堆栈溢出
栈主要是用来保存临时变量,堆是存放对象实例。当递归求解的数据规模较大,一直压入栈,就可能会导致栈溢出
解决办法:在代码中限制递归调用的最大深度。当递归调用超过一定深度之后,就不再继续递归了直接返回报错。
缺点:最大允许的递归深度和当前线程剩余的栈空间大小有关,事前无法计算。
可能存在重复计算
比如计算一个函数F(5)的值,需要计算F(4)和F(3),而计算F(4)仍需要计算F(3),因此F(3)就被重复计算了很多次,存在重复计算问题。
**解决办法:**可以通过一种数据结构(如:散列表、Map)来保存已经求解过的数据,在递归求解之前先判断散列表中是否有对应的结果,如果有就直接取出,没有则继续运算,并将运算结果保存在表中。
可能出现无限递归情况
如果出现脏数据,如:计算A的等于B结果,计算B的结果又等于A的结果,就容易出现死循环
递归代码改为非递归代码(迭代)
递归优点:表达力强,代码简洁
缺点:空间负责度高、有堆栈溢出风险,存在重复计算、过多函数调用耗时较多
//递归
public int steps(int n){
if (n == 1) return 1;
if (n == 2) return 2;
return steps(n - 1) + steps(n - 2);
}
//非递归(迭代)
public int steps2(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
int count = 0;
int pre = 2;
int prepre = 1;
for (int i = 3; i <= n; i++) {
count = pre + prepre;
prepre = pre;
pre = count;
}
return count;
}
几乎所有的递归都可以改写为迭代循环,因为递归本身就是借助栈来实现的,只不过我们使用的栈是系统或者虚拟机本身提供的,我们没有感知罢了。如果我们自己在内存堆上实现栈,手动模拟入栈、出栈过程,这样任何递归代码都可以改写成看上去不是递归代码的样子。
但是,将递归改为了“手动”递归,本质并没有变,而且也并没有解决前面讲到的某些问题,徒增了实现的复杂度