斐波那契(Fibonacci)的定义:
F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
我们可以根据定义实现for循环写法:
function fib1 (n) {
if (n === 1) return 1
if (n === 2) return 1
let prePre= 1 // fib(1)
let pre = 1 // fib(2)
let result // fib(n)
for (let i = 3; i <= n; i++) {
result = prePre + pre // 得到fib(n) = fib(n-1) + fib(n-2)
[prePre, pre] = [pre, prePre + pre] // 设置下一轮的prePre和pre
}
return result
}
然后我们发现,返回的result和下一轮的pre是一样的:
function fib2 (n) {
if (n === 1) return 1
if (n === 2) return 1
let prePre= 1 // fib(1)
let pre = 1 // fib(2)
for (let i = 3; i <= n; i++) {
// 设置下一轮的prePre和pre,最终会是[fib(n -1), fib(n)]
[prePre, pre] = [pre, prePre + pre]
}
return pre
}
再然后,我们思考递归是什么意思:每一项都是前两项的和
直到F(1) === 1或者 F(2) === 1
function fib3 (n) {
if (n === 1) return 1
if (n === 2) return 1
return fib3(n - 1) + fib3(n - 2) //顺理成章
}
n = 3 : f3 = f2 + f1 = 1 + 1 = 2
n = 4 : f4 = f3 + f2 = f2 + f1 + f2 = 1 + 1 + 1 =3
n = 5 : f5 = f4 + f3 = f3 + f2 + f2 + f1 = f2 + f1 + f2 + f2 + f1 = 5
…
这就是递归,每一项都只和前面的一项或多项相关,所以最终会是某几个已知的值的加和。
但是如果n很大的话,就会发现相加的项数会很多,每项都是一个函数堆栈,进而导致堆栈溢出。所以JS中有一个尾调用的优化方案,而其中最常见的就是尾递归优化。
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
接下来的函数可能不太好理解:
function fib4 (n, prePre = 1, pre = 1) { // 初始值:f(1)和f(2)
if (n === 1) return pre
if (n === 2) return 1
return fib(n - 1, pre, prePre + pre)
}
能理解不?最后一步调用自身,其实就是:
本来公式中f(n) 和前两项f(n - 1)、f(n - 2)相关
===》
现在想要f(n)只和前一状态f(n - 1)相关(注意这里用的是状态,而不是项)
思考一个问题,任何一个有序的状态,对比一下:
1,1,2,3,5,8,13,21… // F(8) = 21 => F(n) = 21
1,2,3,5,8,13,21… // f(7) = 21 => F(n - 1) = 21
1,2,3,4,5,6,7,8… // F(8) = 8 => F(n) = 8
2,3,4,5,6,7,8… // F(7) = 8 => F(n) = 8
能够理解了吧。ヽ( ̄▽ ̄)ノ
所以这里,我们可以考虑,将初状态放到函数参数中:
f(8) => f(8, 1, 1) // 初状态:1,1
f(8) => f(7, 1, 2) // 初状态:1,2
f(n) => f(n - 1, f(2), f(3)) => f(n-2, f(3), f(4)) =>
f(3, f(n - 2), f(n - 1)) => f(2, f(n - 1), f(n))
再看上面的函数
function fib4 (n, prePre = 1, pre = 1) { // 初始值:f(1)和f(2)
// 迭代中n === 2时就返回了,只有n 确实等于1时执行
if (n === 1) return 1
// 当 n === 2 时,pre === f(n)
if (n === 2) return pre
// 每次迭代,都相当于执行了
// [prePre , pre] = [pre , prePre + pre]
// 也就是执行了一次for循环
return fib(n - 1, pre, prePre + pre)
}
所以还有个类似的题目给你们你思考:
已知一个活细胞每小时分裂一次,一次分裂出一个新细胞,一个新细胞的寿命是3个小时。
提问:一开始只有一个新细胞,求n小时后会有多少活细胞。
。
。
。
。
。
。
。
。
。
。
。
一般这种有规律的题目,可以先列一些枚举值:
f0 = 1
f1 = 2
f2 = 4
f3 = 7
f4 = 13
f5 = 24
然后一下就发现了规律:
f(n) = f(n - 1) + f(n - 2) + f(n - 3)
function survive (n) {
if (n === 0) return 1
if (n === 1) return 2
if (n === 2) return 4
let first = 1
let second= 2
let third= 4
for (let i = 3; i <= n; i++) {
// 设置下一轮的prePre和pre,最终会是[fib(n -1), fib(n)]
[first, second, third] = [second, third, first + second + third]
}
return third
}
改成尾递归
function survive1 (n, first = 1, second = 2, third = 4) {
if (n === 0) return 1
if (n === 1) return 2
if (n === 4) return third
return survive1(n - 1, second, third, first + second + third)
}
如果单纯地思考问题,如何得出(f(n) = f(n - 1) + f(n - 2) + f(n - 3))这个公式呢?
想象一下,当前的活细胞和一小时前的活细胞的关联:
一小时前所有活着的细胞分裂出的新细胞, =》 f(n -1)
一小时前刚出生的新细胞,=》两小时前的活细胞分裂出的 =》 f(n - 2)
一小时前已经活了一小时的细胞,=》 三小时前的活细胞分裂出的 =》 f(n - 3)
一小时前已经活了两小时的细胞此时已经死了,
更早前的细胞早已经化成灰了。