从一个故事开始讲递归

讲个故事

从前有座山,山上有座庙,庙里有个老和尚和一个小和尚,一天,老和尚给小和尚讲了一个故事,故事的内容是“从前有座山,山上有座庙,庙里有个老和尚和一个小和尚,一天,老和尚给小和尚讲了一个故事,故事的内容…”。

这个故事自己套着自己,没完没了,像是某种特征在不断重复,这就是我们今天要将的主题「递归」了。

递归

递归(Recursion)是一种非常广泛的算法,更是一种编程技巧。英文 Recursion 的中文翻译“递归”表达了两个意思“递”+“归”,去的过程叫做递,回来的过程叫做归,在编程语言中递归可以简单理解为:方法自己调用自己,只不过每次调用的时候参数不同而已。

没记错的话,高中数学中的数列有递推公式的概念,递推公式就是将一个递归问题的规律用数学公式的形式表示出来。基本上所有的递归问题都可以使用递推公式来表示。

在数学中,正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,记为n!,并规定1的阶乘1!为1、0的阶乘0!也为1。

阶乘的递归公式为:0! = 1, n! = n & (n-1)!。

例如

5! = 5 * 4!

4! = 4 * 3!

3! = 3 * 2!

2! = 2 * 1!

1! = 1

即 5! = 5 * 4 * 3 * 2 * 1 = 120;

综上,n的阶乘递推公式为 f(n) = n * f(n-1), 其中f(1) = 1。

求5的阶乘完整过程:

f(5)
=> 5 * f(4)
=> 5 * (4 * f(3))
=> 5 * (4 * (3 * f(2)))
=> 5 * (4 * (3 * (2 * f(1)))
=> 5 * (4 * (3 * (2 * 1)))
=> 5 * (4 * (3 * 2))
=> 5 * (4 * 6)
=> 5 * 24
=> 120

先递进,再回归。

那么哪些问题可以使用递归来解决呢

一、递推公式

如果一个问题的解能够拆分成多个子问题的解,拆分之后子问题和该问题在求解上除了数据规模不一样,求解的思路和该问题的求解思路完全相同,也就是说能够找到一种规律,这种规律就是我们说的递推公式,那么这个问题就可以使用递归来求解。

二、终止条件

把一个问题的解分解为多个子问题的解,把子问题的解再分解为子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件。比如上面的故事中就没有终止条件,会无限循环下去,而求解阶乘问题是有终止条件的就是f(1) = 1。

写递归代码的关键就是要找出把一个大的问题拆解成多个小问题的规律,进而找到递推公式,然后还要找到递归终止条件,最后将递推公式和终止条件翻译成相关语言代码即可。编写递归代码的关键就是把它抽象成一个递推公式,不用去想一层层的调用关系,更不要试图用大脑去分解递归的每个步骤,否则容易被绕进去。

Java实现阶乘求解

public int factorial(int n) {
    if(n <= 1) {
      return 1;
    }
    return n * factorial(n - 1);
}

上面代码存在整型溢出问题,改进版如下:

public BigInteger factorial(int n) {
    if(n <= 1) {
      return BigInteger.ONE;
    }
    return BigInteger.valueOf(n).multiply(factorial(n - 1));
}
递归存在的问题
堆栈溢出

递归最常见的问题就是堆栈溢出,比如上述求解阶乘问题的代码,当n=10000的时候就可能会抛出java.lang.StackOverflowError 错误。那么为什么递归代码容易造成堆栈溢出呢

在计算机中,函数调用是通过栈这种数据结构实现的,每调用一个方法就会在栈上创建一个栈帧,方法调用结束后就会弹出该栈帧,在递归调用中每递归一次就要创建一个栈帧,还要保留之前的栈帧,直到遇到结束条件,而栈的大小是有限的,所以递归调用次数太多的话就会导致栈溢出。

解决方法有以下几种

1、使用循环替代递归,缺点是代码逻辑不够清晰,比如阶乘问题的for循环实现的代码

public BigInteger factorial(int n) {
    BigInteger result = BigInteger.valueOf(n);
    if(n <= 1) {
        return BigInteger.ONE;
    }
    for(int i = 1; i < n; i++) {
        result = result.multiply(BigInteger.valueOf(n -i));
    }
    return result;
}

2、限制递归次数,比如递归调用超过一定次数就直接返回或抛出异常。

重复计算

递归还存在重复计算的问题,比如高中数学数列中的斐波那契数列。

斐波那契数列数列指的是这样一个数列:1、1、2、3、5、8、13、21… ,即第1项f(1) = 1,第2项f(2) = 1,第3项f(3) = f(2) + f(1),第n项为f(n) = f(n-1) + f(n-2)。求第n项的值是多少。

有了递推公式f(n) = f(n-1) + f(n-2),那么斐波那契数列的递归实现如下:

public int fibonacci(int n) {
    if(n == 1 || n == 2) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

递归调用的状态图如下
递归重复计算

可以看到f(3)被计算了2次,如果求更大的数比如f(8),那么会有更多的重复计算。那么如何优化呢?

一般我们可以把计算结果缓存起来,比如把f(3)的计算结果保存在散列表(比如Java中的HashMap)中,当再次要计算f(3)的时候,我们先判断下Map中是否包含了f(3)的计算结果,如果包含则直接把计算结果取出来,这样就可以避免重复计算了。

「更多精彩内容请关注公众号geekymv,喜欢请分享给更多的朋友哦」
geekymv

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值