递归和递推
文章目录
什么是递归?
递归讲白了就是A方法自己调用自己。
递归的好处
利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型且复杂的问题层层转化为一个与原问题相似但规模较小的问题求解。递归策略只需少量的程序就可以描述解题过程所需要的多次重复计算。这大大地减少了程序的代码量。递归的能力在于用有限的语句去定义对象的无限集合。
可以理解为:递归是让程序员便利,让电脑麻烦。
递归三要素
1.明确函数功能
对于递归,很重要的一个事就是这个函数的功能是什么,它要完成什么样的一件事。
例如,我定义了一个函数用来算 n 的阶乘n!
(假设n不为0)
int recursion(int n){
}
2.寻找递归边界
所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的边界,即递归结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为何值时递归结束,之后直接把结果返回。这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。
例如,算 n 的阶乘n!
(假设n不为0)中,当 n = 1
时,我们能够直接知道f(1) = 1
。完善我们函数内部的代码,把第二要素加进代码里面,如下
public static int recursion(int n){
if(n == 1){
return 1;
}
}
当 n = 2
时我们也可以直接知道f(n)
的值,所以我们也可以把n = 2
作为递归的结束条件。
只要我们知道参数是什么时,够直接知道函数的结果,那么我们就可以把这个参数作为结束的条件:
public static int recursion(int n){
if(n <= 2){
return n;
}
}
3. 找出函数内在关系式
第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。
说白了,就是要找到原函数的一个内在关系式,比如f(n)
的等价关系式为f(n)=n*f(n-1)
,
找出了这个等价,就可以继续完善我们的代码,我们把这个等价式写进函数里。如下:
public static int recursion(int n){
if(n <= 2){
return n;
}
return f(n-1) * n;
}
以上就是递归三要素。
下面看两个例子,能帮你更好地理解递归的三要素。
求斐波那契数列的第n项f(n)
斐波那契数列:1、1、2、3、5、8、13、21、34…,
即第一项 f(1) = 1,第二项 f(2) = 1…,第 n 项目为 f(n) = f(n-1) + f(n-2)。
求第 n 项的值。
先上代码:
public class Demo05 {
public static void main(String[] args) {
/*用递归得斐波那契数列f(n)的第n项*/
int result = fibonacci(6);
System.out.println(result);
}
public static int fibonacci(int n){
if (n<=2){
return 1;
}else{
return fibonacci(n-1) + fibonacci(n- 2);
}
}
}
思路分析:
- 明确递归函数f(n)的功能
f(n)
的功能是求第n
项的值,代码如下:
public static int fibonacci (int n){
}
- 找出递归结束的条件
通过斐波那契数列的定义我们已经知道f(1) = f(2) = 1
。所以递归结束条件可以为 n <= 2
时,f(n)= = 1
。代码如下:
public static int fibonacci (int n){
if(n <= 2){
return 1;
}
}
第三要素:找出函数的等价关系式
题目已经把等价关系式给我们了:f(n) = f(n-1) + f(n-2)
。
所以最终代码如下:
public static int fibonacci (int n){
if (n<=2){
return 1;
}else{
return fibonacci(n-1) + fibonacci(n- 2);
}
}
Leetcode原题:青蛙跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级。
求该青蛙跳上一个n级的台阶总共有多少种跳法?
依旧还是
先上代码:
public class Demo06 {
public static void main(String[] args) {
/*一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法?*/
int result = frogJump(10);
System.out.println(result);
}
public static int frogJump(int n){
if (n <= 2) {
return n;
}else {
return frogJump(n-1) + frogJump(n-2);
}
}
}
输出结果:
8
思路分析:
- 递归函数功能
假设f(n)
的功能是求青蛙跳上一个n
级的台阶总共有多少种跳法,代码如下:
int frogJump(int n){
}
2、找出递归结束的条件
求递归结束的条件,就是考虑把n
压缩到很小很小就行了,因为n
越小就越容易直观着算出f(n)
的多少,所以当 n = 1
时f(1) = 1
。n=2
时f(2)=2
.代码如下:
int frogJump(int n){
if(n <= 2){
return n;
}
}
第三要素:找出函数的等价关系式
每次跳的时候,青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,青蛙有两种跳法。
第一种跳法:第一次跳了一个台阶,那么还剩下n-1
个台阶还没跳,剩下的n-1
个台阶的跳法有f(n-1)
种。
第二种跳法:第一次跳了两个台阶,那么还剩下n-2
个台阶还没,剩下的n-2
个台阶的跳法有f(n-2)
种。
所以,青蛙的全部跳法就是这两种跳法之和了,即f(n) = f(n-1) + f(n-2)
。至此,等价关系式就求出来了。于是写出代码:
int frogJump(int n){
if(n<= 2){
return n;
}else{
ruturn frogJump(n-1) + frogJump(n-2);
}
}
有关递归的一些优化思路
为什么要优化递归?
如果使用递归的时候不进行优化,是有非常非常非常多的子问题被重复计算的。(子问题就是f(n-1),f(n-2)…就是 f(n) 的子问题。)
例如对于案例2那道题,f(n) = f(n-1) + f(n-2)
。递归调用的状态图如下:
可见递归计算的时候,重复计算了两次 f(3),1次 f(4)。。。。n 越大,重复计算的就越多,所以我们必须进行优化。
如何优化?
1. 把计算结果保存起来直接调用
一般我们可以把我们计算的结果保存起来,例如把 f(4) 的计算结果保证起来,当再次要计算 f(3) 的时候,我们先判断一下,之前是否计算过,如果计算过,直接把 f(3) 的结果取出来就可以了,没有计算过的话,再递归计算。
用什么保存呢?可以用数组或者HashMap保存。
用数组来保存:把n
作为我们的数组下标,f(n)
作为值,例如arr[n] = f(n)
。f(n)
还没有计算过的时候,我们让 arr[n] 等于一个特殊值,例如arr[n] = 0
。
当我们要判断的时候,如果 arr[n] = 0
,则证明f(n)
没有计算过,否则, f(n)
就已经计算过了,且f(n) = arr[n]
。直接把值取出来就行了。
以斐波那契数列那题为例。代码如下(假定 arr 数组已经初始化完成):
int fibonacci(int n){
if(n <= 1){
return n;
}
//先判断有没计算过
if(arr[n] != 0){
//计算过,直接返回
return arr[n];
}else{
// 没有计算过,递归计算,并且把结果保存到 arr数组里
arr[n] = fibonacci(n-1) + fibonacci(n-1);
reutrn arr[n];
}
}
也就是说,使用递归的时候,必须要考虑有没有重复计算,如果重复计算了,一定要把计算过的状态保存起来。
2. 考虑是否可以自底向上
对于递归的问题,我们一般都是从上往下递归的,直到递归到最底,再一层一层着把值返回。
当n
比较大的时候,例如当n = 10000
时,必须要往下递归10000层直到 n <=1
才将结果慢慢返回,如果n太大的话,可能栈空间会不够用。对于这种情况,我们是可以考虑自底向上的做法。
例如已知
f(1) = 1
f(2) = 2
那么我们就可以推出f(3) = f(2) + f(1) = 3
。从而可以推出f(4),f(5)…直到f(n)。因此,我们可以考虑使用自底向上的方法来取代递归,代码如下:
public int fibonacci(int n) {
if(n <= 2)
return n;
int f1 = 1;
int f2 = 2;
int sum = 0;
for (int i = 3; i <= n; i++) {
sum = f1 + f2;
f1 = f2;
f2 = sum;
}
return sum;
}
这种方法,其实也被称之为递推。
总结
递归不一定总是从上往下,也是有很多是从下往上的。