斐波那契数列,又称黄金分割数列,是一种递归的数列,以它的递推公式和美丽的数学性质得到了广泛的应用。本文将介绍使用 JS 实现斐波那契数列的几种方法,包括递归和循环,以及优化方案等。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
斐波那契数列的定义
斐波那契数列(Fibonacci sequence)是一种递归的数列,以它的递推公式和美丽的数学性质得到了广泛的应用。它的定义如下:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)。也就是说,第 n 个斐波那契数就是第 n-1 个和第 n-2 个斐波那契数之和。通常用 F(n)表示斐波那契数列中的第 n 个数。
下面让我们来看看如何解决这个问题吧。
以终为始——递归实现
普通递归
对于普通的递归我的总结就是:以终为始,套娃计算,适时终止。
- 以始为终:用倒推的方式来思考。
- 套娃计算:找到套娃的公式。
- 适时终止:递归到能直接得到结果的地方就停并返回结果。
记得以前做数学题的时候,有时候一步一步想不出题目的答案的时候,但却能猜到答案,于是就从答案倒推过程。不要管解题过程优雅不优雅,写的快就行。
有点扯远了,下面我们来开始倒推。从后往前倒推就是 :
// F(n)=F(n-1)+F(n-2)
// F(n-1)=F(n-2)-F(n-3)
// ...
// F(3)=F(2)+F(1)
// F(2)=1
// F(1)=0
我们可以得到:
- 公式:F(n)=F(n-1)+F(n-2)
- 终止条件:n=2,和 n=1
最终代码实现如下:
function fibonacci(n) {
if(n < 1) return 0;
if(n <= 2) return 1;
return fibonacci(n - 1) + fibonacci(n - 2)
}
// 验证
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(3)
fibonacci(4)
fibonacci(5)
// ...
// 下面函数在浏览器中调用会导致栈溢出
// fibonacci(50)
JavaScript 代码运行时,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。
F(50)调用 F(49) 和 F(48),F(49)调用 F(48) 和 F(47),...
以此类推,所有的调用记录,就形成一个“调用栈”(call stack)。
如果栈深度过大,就会导致堆栈溢出。
想知道当前浏览器栈调用大小?可以用下面的代码进行测试:
let i = 0;
function inc() {
i++;
inc();
}
try {
inc();
} catch(e) {
console.log('你的浏览器中最大的调用栈是:', i);
}
缓存重复计算
上面提到 fibonacci(50) 在浏览器中执行因为栈溢出会导致浏览器卡死的情况,让我们来优化一下。
观察下图我们不难发现,两个黄色实线圈和两个紫色的虚线的地方,都出现了重复计算,fibonacci(n) 中的 n 越大,重复计算的地方越多。如果我们能缓存每次计算的结果,就能减少重复计算,提升运行效率。
代码实现:
function fibonacci(n) {
// 利用闭包缓存计算的结果
const memo = [0, 1];
function fib(n) {
if (memo[n] === undefined) {
return memo[n] = fib(n - 2) + fib(n - 1);
}
return memo[n];
}
console.log(fib(n));
return fib(n);
}
// 验证
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(3)
fibonacci(4)
fibonacci(5)
// ...
fibonacci(50)
规避重复计算(尾调优化)
尾调用优化是一种有效的优化技术,可以减少函数调用次数,从而提升递归函数的效率。它的原理是将当前函数的返回值作为另一个函数的参数,并将函数的调用放到最后(即尾调),从而减少函数调用和堆栈的深度。这样,就可以让函数能够在堆栈中以更少的深度运行,从而提高函数的效率。
// 函数中多了两个参数不太优雅?传个参数默认值就可以解决!
function fibonacci(n, sum = 0, last = 1) {
if (n === 0) {
console.log(sum)
return sum;
}
return fibonacci(n - 1, last, sum + last);
}
// 验证
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(3)
fibonacci(4)
fibonacci(5)
// ...
fibonacci(50)
手法优雅——循环实现
普通 for 循环
使用普通的 for 循环实现斐波那契数列,只需要将前两个数字作为起始值,每次循环加上前两个数字的和,即可实现斐波那契数列。
function fibonacci(n) {
const arr = [0, 1];
for (let i = 2; i <= n; i++) {
arr[i] = arr[i-2] + arr[i-1];
}
console.log(arr[n])
return arr[n];
}
// 验证
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(3)
fibonacci(4)
fibonacci(5)
// ...
fibonacci(50)
空间上的优化
就是省一个数组😂,如果只求第 n 个的值,也就没必要用数组保存了。直接看代码:
function fibonacci(n) {
let sum = 0;
let last = 1;
for (let i = 1; i <= n; i++) {
const tmp = sum;
sum = last;
last = tmp + last;
}
console.log(sum)
return sum;
}
// 验证
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(3)
fibonacci(4)
fibonacci(5)
// ...
fibonacci(50)
ES6 解构赋值
上面的代码看的有些许难受,因为循环的时候引入了一个临时变量 tmp,其实我们用 ES6 中的解构赋值代码可读性会更好。
function fibonacci(n) {
let sum = 0;
let last = 1;
for (let i = 1; i <= n; i++) {
[sum, last] = [last, sum + last]
}
console.log(sum)
return sum;
}
// 验证
fibonacci(0)
fibonacci(1)
fibonacci(2)
fibonacci(3)
fibonacci(4)
fibonacci(5)
// ...
fibonacci(50)
ES6 Generator 函数写法
给炒冷饭加个汤 🥣,就说喝不喝吧 🐶
ES6 中的 Generator 函数是一种可以在函数调用时,暂停和恢复运行的函数。
Generator 函数返回的是一个迭代器,因此可以使用 for...of... 语法进行遍历。for...of... 循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,从而不需要再调用 next 方法。这样,就可以对 Generator 函数中 yield 生成的数据进行处理。
是不是发现了:这里使用 Generator 函数和 for...of... 来写斐波那契数列也挺合适。
function* fibGenerator() {
let [prev, curr] = [0, 1];
while (true) {
yield prev;
[prev, curr] = [curr, prev + curr];
}
}
function fibonacci(n) {
let index = 0;
for (let value of fibGenerator()) {
if (index++ < n) continue;
console.log(value);
return value;
}
}
// 验证
fibonacci(0) // 0
fibonacci(1) // 1
fibonacci(2) // 1
fibonacci(3) // 2
fibonacci(4) // 3
fibonacci(5) // 5
// ...
fibonacci(50) // 12586269025
如果想学习更多关于 Generator 函数的用法,请点击这里。(阮一峰老师 🧑🏫 讲的真的很不错!)