什么是递归?什么是递归回溯?如何使用递归?
注:本文默认对递归有一定了解,所以刚开始会 cover 一些简单的例子
一、何为递归
何为递归?程序反复调用自身即是递归。
用数学代入法来理解就好。
假设我们用递归来算阶乘 f(n)
f(n) = n * f(n-1)
f 里面用到了 f, 怎么理解呢?
很简单,把式子展开即可:
f(6)
=> 6 * f(5)
=> 6 * (5 * f(4))
=> 6 * (5 * (4 * f(3)))
=> 6 * (5 * (4 * (3 * f(2))))
=> 6 * (5 * (4 * (3 * (2 * f(1)))))
=> 6 * (5 * (4 * (3 * (2 * 1))))
=> 6 * (5 * (4 * (3 * 2)))
=> 6 * (5 * (4 * 6))
=> 6 * (5 * 24)
=> 6 * 120
=> 720
看到递归了吗?
先递进,再回归——这就是「递归」。
二、递归的重要组成部分
明白了什么是递归之后,我们就要从一个抽象的层面上来对他抽丝剥茧,究竟什么构成了一个递归?
首先,要继续接下面的内容,我们需要引入一个概念——栈帧 (stack frame)。我们可以把栈帧简单理解成一层层的盒子,每当我们调用一次函数,关于该函数的调用以及返回地址就会被放到栈帧的顶上。拿阶乘举例,我们最后画出来的栈帧就是这样的:
我们可以看到在栈帧的顶上是f(1),到这个时候我们就没有去计算所谓的f(0)了。这是因为我们现在到了一个递归的终止条件。顾名思义,当到这个地方时阶乘就不会继续往下,因为没有了意义。
第二步,我们要明白每一层要给上一层提供什么信息。继续看阶乘的算法,我们可以发现,每一层都会返回一个n * f(n-1)。其中这个就是我们留下来的信息,而这个信息就会被逐步返回,直到返回第一层。这也叫做 recursive case。也就是没有到终止条件时,递归会做什么。
总的来说,有两个条件在递归中非常重要:
- 递归的终止条件
- 没有到终止条件时做的事情
三、递归的例子
接下来我们看一下如何真正的实现一个递归:
阶乘
首先,阶乘是递归的一个经典问题,因为我们已经发现了阶乘的递推的公式
f(n) = n * f(n-1)
所以我们很快就可以写出如下代码:
int factorial(int n){
if(n == 0){
return 1;
}
return n * factorial(n - 1);
}
简单分析一下,我们的递归结束条件就是 n == 0,因为在这里我们没有继续再调用自己往下算了。
接着,我们也实现了我们在递归时要做的东西,即n * f(n - 1)。可以想象一下,没有这一部分,我们是没有办法能够把这个阶乘问题划分成更小的子问题的。所以这一部分是必须有的。
所以,一个非常重要的点就是,你要确保你的函数在每次递归之后,都能够解决一点原来的问题。这也叫做问题的分解,这也是递归的精髓所在——将原问题不断拆分为与原问题等价的小问题。
斐波那契数列
斐波那契数列的是这样一个数列 :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 项的值是多少。
首先,