第一要素:明确你这个函数想要干什么:
第二要素:寻找递归结束条件:
所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。
只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。
第三要素:找出函数的等价关系式:
第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。
说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即
f(n) = n * f(n-1)。
如果例如:逆转单链表。可以先截取前三个节点作为尝试,看看是如何缩小范围的。(head.next)。
有关递归的一些优化思路:
1. 考虑是否重复计算:
如果你使用递归的时候不进行优化,是有非常非常非常多的子问题被重复计算的。
啥是子问题? f(n-1),f(n-2)....就是 f(n) 的子问题了。
如何优化?一般我们可以把我们计算的结果保证起来,例如把 f(4) 的计算结果保证起来,当再次要计算 f(4) 的时候,我们先判断一下,之前是否计算过,如果计算过,直接把 f(4) 的结果取出来就可以了,没有计算过的话,再递归计算。
用什么保存呢?可以用数组或者 HashMap 保存,我们用数组来保存把,把 n 作为我们的数组下标,f(n) 作为值,例如 arr[n] = f(n)。f(n) 还没有计算过的时候,我们让 arr[n] 等于一个特殊值,例如 arr[n] = -1。如下:
// 我们实现假定 arr 数组已经初始化好的了。
int f( int n ){
if (n <= 1)
{ return n; }
//先判断有没计算过
if (arr[n] != -1)
{ //计算过,直接返回 return arr[n]; }
else { // 没有计算过,递归计算,并且把结果保存到 arr数组里
arr[n] = f(n-1) + f(n-2);
reutrn arr[n];
}
}
2. 考虑是否可以自底向上:
对于递归的问题,我们一般都是从上往下递归的,直到递归到最底,再一层一层着把值返回。不过,有时候当 n 比较大的时候,例如当 n = 10000 时,那么必须要往下递归10000层直到 n <=1 才将结果慢慢返回,如果n太大的话,可能栈空间会不够用。
那么我们就可以推出 f(3) = f(2) + f(1) = 3。从而可以推出f(4),f(5)等直到f(n)。因此,我们可以考虑使用自底向上的方法来取代递归,代码如下:这种方法,其实也被称之为递推。
在递归过程中注意:参数的选择决定递归的收缩。递归的转化可以先模拟一轮二轮三轮的递归操作(拆解为一次和剩下所有次数组成的一堆)。
最后剩下就是解决return的问题: