问题
有人想知道一年内一对兔子可繁殖成多少对,便筑了一道围墙把一对兔子关在里面。已知一对兔子每一个月可以生一对小兔子,但是一对兔子要从出生后第三个月才开始生小兔子假如一年内没有发生死亡,则一对兔子一年能繁殖成多少对?
代码
function f(n){
// 先用一个数组,保存第一个月和第二个月兔子数量
var Fibonacci = [1,1];
for(var i=2;i<n;i++){
Fibonacci[i] = Fibonacci[i-1] + Fibonacci[i-2]
}
return Fibonacci[n-1];
}
f(12);
运行结果
f(12) // 144
一年后,共有144对兔子
分析
解决这种问题,我们一定要找到其中的规律,看下图
从图中我们能看到的规律:
从第三个月开始,第n个月兔子的总对数=第(n-1)月兔子的总对数+第(n-2)月兔子的总对数,也就是前面相邻两项之和,构成了后一项。
仔细思考这个规律,我们来写代码,这个规律其实就是在不断的把前两项相加,得到后一项,不断的重复这个事情,想到这我们应该会想到用循环来写,第一个月和第二个月比较特殊,我们用数组先保存下,然后就简单了,不断的把前两个月的数量相加,然后保存就好了。
代码
function f(n){
// 先用一个数组,保存第一个月和第二个月兔子数量
var Fibonacci = [1,1];
console.log(`第1个月,共有${Fibonacci[0]}对兔子`);
console.log(`第2个月,共有${Fibonacci[1]}对兔子`);
for(var i=2;i<n;i++){
Fibonacci[i] = Fibonacci[i-1] + Fibonacci[i-2];
console.log(`第${(i+1)}个月,共有${Fibonacci[i]}对兔子`);
}
return Fibonacci[n-1];
}
f(12);
运行结果
总结
现在问题算是解决了,我们再来看一种代码量更少的实现方法
function f(n){
if(n == 1 || n == 2){
return 1;
}
return f(n-1)+f(n-2);
};
console.log(`共有${f(12)}对兔子`);
这种方法就是利用了递归。
递归是指函数/过程/子程序在运行过程序中直接或间接调用自身而产生的重入现象。
递归指的是一个过程:函数不断引用自身,直到引用的对象已知。
说的简单点,递归就是在一个函数内,又调用了自己,但是递归调用的内层函数,是在外层函数还未结束时就已经开始了,外层函数的调用,就会被阻塞,所以缺点就是,算法复杂度太高,而且太浪费内存。
比如说我们算 f(12) 那么我们会计算 f(11) 和 f(10) ,而计算 f(11) 时,又要计算 f(10),这就很浪费了。
所以这个递归需要优化下,看下面的代码。
function f(n , ac1 = 1 , ac2 = 1) {
if( n == 1 || n ==2 ) {
return ac2
};
return f(n - 1, ac2, ac1 + ac2);
}
console.log(`共有${f(12)}对兔子`);
这段代码是通过 尾递归 优化后的样子。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
至于尾递归具体是怎么回事,阮一峰老师已经说得非常好了,想了解的朋友可以看看那这里
而斐波那契数列,就是我们得到的那段数字 1,1,2,3,5,8,13,21,34,55,89,144,至于更加详细的可以自己查查看!