JS:理解递归从斐波那契函数开始

斐波那契(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)
一小时前已经活了两小时的细胞此时已经死了,
更早前的细胞早已经化成灰了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值