当前位置: 首页 > Web前端 > HTML

面试官:用“尾递归”优化斐波那契函数

时间:2023-03-28 00:02:03 HTML

1前言编程题:输入一个整数n,输出斐波那契数列的第n项有些面试官喜欢问这个问题。可能你觉得这个太简单了,用递归或者递归一下子就可以实现。就在你信心满满,用两种方式实现的时候...面试官:现在请用“尾递归”来优化你的递归实现,用“ES6解构赋值”来优化你的递归实现...这个时候,如果你的基本功不扎实,你可能会一头雾水。这么简单的一道题,里面包含了相当多的JS知识点,尤其是它的优化过程更能说明你的基本功不扎实,所以有些面试官喜欢问这个问题。下面我们来看递归和递归的两种实现方式以及各自的优化过程=1){returnn;}returnfibonacci(n-1)+fibonacci(n-2);}让我们看看这段代码有什么问题。第一个问题很容易看出。当n很大时,递归深度过大导致栈内存溢出,即“栈爆炸”。第二个问题是重复计算相当多,比如:fibonacci(7)=fibonacci(6)+fibonacci(5)//这里计算f(5),f(5)=(fibonacci(5)+fibonacci(4))+(fibonacci(4)+fibonacci(3))下面再计算f(5)...2.2Tailcall在解决上面两个问题之前,我们先了解什么是tailcallTailcall:函数中的最后一个动作是返回函数调用的结果,即上一步新调用的返回值由当前函数返回。例如:functionf(x){returng(x)}以下情况不是尾调用:functionf(x){returng(x)+1//先执行g(x),最后返回returnofg(x)value+1}functionf(x){letret=g(x)//先执行g(x)returnret//最后返回g(x)的返回值}2.3尾调用消除(尾调用优化)一当一个函数被调用时,JS引擎会创建一个新的栈帧,并将其压入调用栈的顶部,代表本次函数调用。当发生函数调用时,计算机必须“记住”调用函数的位置——返回位置,调用结束时可以用返回值返回到这个位置,返回位置一般保存在调用栈中.在尾调用的特殊情况下,计算机理论上不需要记住尾调用的位置,而是直接用被调用函数的返回值返回当前函数的返回位置(相当于在一个函数中直接返回两次)row)如下图所示:由于尾调用,理论上可以不记住位置2,而是直接从函数g返回到位置1,返回值(即函数f的返回位置)。由于尾调用之前的工作已经完成,当前函数帧(即调用时创建的栈帧)包含了局部变量,大部分东西已经不需要了。适当改动后,可以直接将当前函数帧作为尾调用函数的帧,程序就可以跳转到被调用的尾调用函数。用上图的例子来说明,函数f尾调用函数g之前的工作已经完成,所以调用函数f时创建的函数帧直接被函数g使用,不需要再再次为函数g创建堆栈帧。这种更改和重用函数帧的过程称为尾调用消除或尾调用优化。2.4尾递归如果一个函数在尾调用的位置调用自己,则称为尾递归。尾递归是一种特殊的尾调用,即最后直接调用自身的递归函数。由于消除了尾调用,尾递归只有一个栈帧,因此永远不会“爆栈”。ES6规定所有的ECMAScript实现都必须部署“尾调用消除”,所以在ES6中只要使用尾递归,就不会出现栈溢出2.5尾递归优化斐波那契函数回到2.1斐波那契函数存在对于这两个问题,可以使用尾递归解决“爆栈”问题。需要注意的是,ES6尾调用消除仅在严格模式下启用。最后一步调用自身//原始递归函数functionfibonacci(n){if(n===0||n===1){returnn;}返回斐波那契(n-1)+斐波那契(n-2);}//修改后的'usestrict'functionfibonacci(n,pre,cur){if(n===0){returnn;}如果(n===1){返回当前;}returnfibonacci(n-1,cur,pre+cur);}//调用fibonacci(6,0,1)后修改后的计算逻辑如下:f(6,0,1)=f(5,1,0+1)=f(4,1,1+1)=f(3,2,1+2)=f(2,3,2+3)=f(1,5,3+5)=8你可能发现了,其实这就是递归,从0+1开始,一直到第n项另外可以使用ES6的默认参数让函数只传入一个参数n来'usestrict'函数fibonacci(n,pre=0,cur=1){如果(n===0){返回n;}如果(n===1){返回当前;}返回斐波那契数(n-1,cur,pre+cur);}fibonacci(6)此外,由于函数改为尾递归形式,第f(n)次只与f(n-1)相关,大量重复计算的问题也得到解决解决了。3递归递归实现函数fibonacci(n){letcur=0;让下一个=1;对于(设i=0;i