【问题标题】:JavaScript Fibonacci breakdownJavaScript 斐波那契细分
【发布时间】:2013-09-29 14:31:56
【问题描述】:

我希望我可以在这里发布这个问题,即使我也在其他网站上发布了它。如果我未能遵守正确的协议,我深表歉意,并请立即通知我,以便我删除帖子并吸取教训。

我做前端开发人员已经一年多了。我上学是为了学习 Web 开发,我认为自己在处理简单的 JavaScript 方面是一个有能力的程序员。 但是当涉及到编写任何类型的斐波那契函数时,我无法做到。就好像我的大脑中缺少了一块可以理解如何处理这个简单数字序列的东西。 这是一段工作代码,我很确定我是从 John Resig 的书或网上某处获得的:

fibonacci = (function () {
    var cache = {};
    return function (n) {

        var cached = cache[n];
        if (cached) return cached;

        if (n <= 1) return n;
        console.log(n);

        return (cache[n] = fibonacci(n - 2) + fibonacci(n - 1));
    };
}());

当我以 10 作为参数调用这个函数时,我得到这个序列:10,8,6,4,2,3,5,7,9

这是我的理解:

fibonnaci 被分配了一个立即调用的函数表达式(或自执行等等等等),无论传递什么参数,都会向其初始化一个缓存。 如果争论已经在缓存中,我们只需将其归还并过着永恒的和平生活。 如果参数为 1 或更少,这也是函数的结束,永恒的和平再次发生。 但是如果这两个条件都不存在,那么函数会返回这个让我感觉自己只是一个穿人套装的猴子的语句。

我想做的是按照正确的顺序生成前 10 个斐波那契数,因为如果我能做到,那么我会觉得我至少理解了它。

因此,当前两个条件失败时,代码会创建一个新的缓存变量,并将其设置为等于斐波那契函数的结果,无论传递的参数如何减去 2,然后将结果加上减去 1.... 现在我的问题

  • 问题 1:如果从未计算过 fibonacci(n),函数如何知道 fibonacci(n-2) 是什么?
  • 问题 2:递归函数是线性的,还是它们遵循什么顺序?
  • 问题3:如果我不能理解这个,我还有成为程序员的希望吗?

感谢您的宝贵时间。

在通过这个块之后,我稍微改变了函数,看看我是否可以将结果保存在一个变量中并输出它,只是为了看看会发生什么,我得到了一些非常意想不到的结果。

变化如下:

fibonacci = (function () {
        var cache = {};
        return function (n) {

            var cached = cache[n];
            if (cached) {
                console.log(cached);
                return cached;
            }

            if (n <= 1) {
                console.log(n);
                return n;
            }
            console.log(n);

            var result = (cache[n] = fibonacci(n - 2) + fibonacci(n - 1));
            console.log(result);
            return result;
        };
    }());

这是生成的模式: 10,8,6,4,2,0,1,1,3,1,1,2,3,5,2,3,5,8,7,5,8,13,21,9,13, 21,34,55 对为什么会发生这种情况有任何帮助吗?

【问题讨论】:

  • 你试过调试吗?您将能够看到整个过程。
  • 我有……在得到别人的回应后,我现在还有一个问题:我的大脑是线性思考的。这意味着首先发生一个步骤,然后发生其他事情,依此类推,直到有最终结果。但是当我看到递归函数的结果时,它们似乎一直在计算,直到它们满足最终条件,然后它们爆发出所有其他他们不知道最初问题的答案。这是对它们如何工作的正确理解吗?
  • @ClasslessAndFree:嗯,我没有详细介绍,但是 JavaScript(嗯,ECMAScript,up until ES5 strict mode)实际上并没有很好地进行递归(没有 TCO)。实际上,递归调用被处理为goto 语句......这就是使它们更难单步调试和调试的原因。一开始它们也很难掌握,但一旦你掌握了它们,你就会爱上它们
  • @ClasslessAndFree:顺便说一句,您还在哪里发布了这个问题?我很想看看其他回复...
  • 为什么人们一直这样做?像斐波那契这样的函数是递归的可怕例子。除非该语言具有内置的记忆功能,否则递归解决方案绝对没有迭代解决方案的优势。

标签: javascript caching fibonacci


【解决方案1】:

好吧,让我们从你理解的(或说你理解的)开始:

fibonnaci 被分配了一个立即调用的函数表达式(或自执行等等等等),无论传递什么参数都会向其初始化缓存。

不完全是:fibonnaci 被分配了IIFE 的返回值。有区别。在 IIFE 中,我们看到了 return function(n) 声明。 IIFE,顾名思义,立即调用。该函数被创建、执行,并且一旦返回,就不会在任何地方(显式)引用。函数返回,赋值给变量fibonacci
这个 IIFE确实创建了一个对象字面量,称为 cache。这个对象位于 IIFE 的范围内,但是because of JS's scope scanning(这个答案链接到其他人......他们都一起解释了 JS 如何将名称解析为它们的值),这个对象仍然可以被返回的函数访问,现在分配给斐波那契。

如果参数已经在缓存中,我们只需将其归还并过着永恒的和平生活。如果参数为 1 或更少,这也是函数的结束,永恒的和平再次发生。但是[...]

好吧,现在cache 不会在每个函数调用上一遍又一遍地创建(IIFE 只调用一次,这就是创建cache 的地方)。如果返回的函数 (fibonnaci) 对其进行了更改,则对对象的更改将保留在内存中。闭包变量,因为这就是 cache 可用于在函数调用之间保持状态。你说的所有其他东西 (n &lt;= 1) 都是标准递归函数的东西......它是阻止无限递归的条件。

但如果这两个条件都不存在,那么该函数会返回这个语句,让我觉得自己只是一只穿着人类西装的猴子。

嗯,这实际上是有趣的部分。这就是真正的魔法发生的地方。
正如我所解释的,fibonnaci 没有引用 IIFE,而是引用了返回的函数:

function(n)
{
    var cached = cache[n];
    if (cached)
    {
        return cached;
    }
    if (n <= 1)
    {
        return n;
    }
    return (cache[n] = (fibonacci(n-2) + fibonnaci(n-1)));
}

这意味着,fibonacci 的每一次出现都可以替换为函数体。调用fibonnaci(10)时,最后一个(return)语句应该读作:

return (cahce[n] = fibonacci(8) + fibonnaci(9));

现在,如您所见,fibonacci(8)fibonnaci(9) 在返回值中被调用。这些表达式也可以写成完整的:

return (cache[10] = (fibonnaci(6) + fibonacci(7)) + (fibonacci(7) + fibonacci(8)));
//which is, actually:
return (cache[10 = ( retrun (cache[6] = fibonacci(4) + fibonacci(5))
                   //since fibonacci(6) cached both fibonacci(5) & fibonacci(6)
                     + return (cache[7] = cache[5] + cache[6])
           + return cache[7] + return (cache[8] = cache[6] + cache[7]

这就是这个缓存函数的实际联系方式......

我们可以重复这个直到我们调用fibonnacci(1)fibonacci(0),因为在这种情况下n&lt;=1,并且将在没有任何递归调用的情况下返回。
另请注意,在完整编写fibonnaci(9) 时,这实际上分解为fibonacci(7) + fibonacci(8),这两个调用之前都已进行过,这就是缓存结果的原因。如果你不缓存每次调用的结果,你会浪费时间计算两次相同的东西......

顺便说一句:这段代码非常“condesed”,因为规范说赋值表达式 (cache[n] = ...) 的计算结果为赋值,所以你返回 cache[n],基本上。

【讨论】:

  • 好的,您的回答非常有帮助,因为您填补了我知识的一些严重空白。不过我又得到了一个……当我调用 fibonacci(10) 时,我是否返回了 10 次调用同一函数的结果?
  • @ClasslessAndFree:我发布的答案太快了,我已经扩展了一些关于您应该解释fibonnaci(n-2) + fibonnaci(n-1) 位的人。基本上,一开始 n 是 10,然后使用 n-2 ==&gt; 10 -2 === 8 重复相同的功能,然后将其分解为 2 个调用(8-2 和 8-1),依此类推,直到到达 fibonnaci(1),它不会再次调用自己,而只是返回 1,这会停止递归...还添加了一个链接,请检查链接答案底部的链接(将其视为递归链接:-P)跨度>
  • 好的,谢谢!我还编辑了我的初始问题,因为我做了一些更改。你知道为什么我在做出改变后得到了这个结果吗?这不是什么大不了的事,只是看起来很奇怪。
  • @ClasslessAndFree:您获得的日志是完全合理的:result 将被分配 行的总和,如目前计算的那样(这是一个递增序列,如下this pattern)。由于您正在调用 fib(10),因此记录的最后一个值实际上是 55。因为您还添加了 console.log(cached) 语句,所以您还记录了所有内容(在第一次调用之后) double: 10 被分解为 8 的 recusive 调用和 9, 9 被分成 8 和 7,而 8 被分成 7 和 6。所以每个数字调用函数 两次
  • 让我们试着写完整的调用:fibonacci(10): = f(8) + f(9) = f(6) + f(7) + f(7) + f(8) = f(4) + f(4) + f(5) + f(5) + f(6) + f(5) + f(6) + f(6) + f(7) = ... 好的,我不能被打扰......但基本上,你明白了......其中一些调用您会多次看到:因为每个调用都可以分解为 同一函数的前 2 个整数的返回值之和 f(10) = (f(8) + (f(9 )) 和f(8) = (f(6) + f(7)),但是因为f(8)f(9) 的递归函数都将处理0,1,2,4,5,6,7,所以您将记录两次...
【解决方案2】:

好问题。用递归的方式思考是很棘手的。如果你试图掌握所有的过程,你可能会惨败。我记得我很沮丧,因为您不了解 Hanoi towers 问题的递归解决方案。我试图在纸上追踪步骤的顺序,但这对理解正在发生的事情的魔力没有任何帮助。

它对我有用的是认为递归函数是一种“oracle”,它知道函数fib(i) 对于i &lt; n 的任何值的返回值。如果 oracle 知道 fib(n-1)fib(n-2),那么 它只需要给出指令以根据这些已知值计算 fib(n) 因此我们可以说oracle 这个函数也知道fib(n) 的值。

警告:所有递归函数都有一个棘手的部分,我们至少需要一个在开始过程时已知的非递归值,否则我们将有无限递归。在斐波那契示例中,这些值为:fib(0) = 0fib(1) = 1

您的示例比这要复杂一些,就像使用 memoization 一样,这是一种将 fib(n) 的值存储在缓存中以避免重新计算它们的技术。在这种情况下,此缓存是一个稀疏数组(带有“孔”的数组),它在第一次计算时将fib(i) 的值存储在其位置i。这是一种避免在下次请求 fib(i) 的相同值时重复昂贵计算的方法。

回答您的问题:

  1. fib(n-2) 不需要知道fib(n) 的值来计算,它需要的是fib(n-3)fib(n-4) 的值。唯一要做的就是调用它们来询问“神谕”它们的值。
  2. 这取决于,有线性递归函数和树形递归函数,这取决于它们使用了多少其他值。在这种情况下,您有一个树形调用顺序。 (实际上记忆化使它更复杂,我会将其表示为有向无环图而不是树,但这与讨论无关)。
  3. 继续想一想,总有一天你会有一个“啊哈”的时刻,然后递归函数对你来说会变得很明显。

如果您仍想跟踪此函数的执行,也许它将有助于将您的计算重构为等效方式,因为它使执行顺序更加明显:

// var result = (cache[n] = fibonacci(n - 2) + fibonacci(n - 1));

var fib2 = fibonacci(n - 2); 
var fib1 = fibonacci(n - 1);

var result = fib2 + fib1;
cache[n] = result;

【讨论】:

  • 我不得不说,你对问题 #2 的回答刚刚为我打开了一个新世界......这就是我真正的目标,也是我的兴趣所在(以及挫败感)我最。我搜索了有向无环图只是为了看一张图片,这就是我的输出似乎对应的内容。
  • 好答案,但我必须不同意 "非循环图" 位。无论返回值是计算出来的还是来自cache 对象,对任何n-x 值的第一次递归调用都将导致另一个调用序列,或者返回1 || 0,因此是一棵正则树。这是因为函数在每次递归时被调用两次,你会得到一堆树,因此 => Fibonacci heap 被列为树数据结构
  • 好的...谢谢你让我明白这一点。我以前从未听说过,并开始脱离我的主要话题
【解决方案3】:

我知道这个问题有点老了,答案很有帮助。我在 GoLang 中做这个练习,并思考如何用 Javascript 编写,并使用这个答案来刷新我的想法。我看到您的代码有一个缓存变量来存储 fib(n-2) + fib(n-1) 迭代的值。如果进行递归,则不需要变量来存储结果,因为每次调用函数时都会返回一个数字,并且这些数字会累加到第一次函数调用中。

function fib(n) {
    if (n<=1) return n;
    return fib(n - 1) + fib(n - 2);
}

要查看为什么不需要缓存变量,请遍历每个函数调用,并在 n 等于 1 或 0 时开始计算值。

例如:

iteration 1)

fib(3) {
   return fib(3-1) + f(3-2)   
}
---------------------------    
iteration 2)

fib(3-1) {
   return fib(2 - 1) + fib(2-2)
}

iteration 3)

fib(3-2) {
   return 1
}    
---------------------------

iteration 4)

fib(2-1) {
   return 1
}


iteration 5)
fib(2-2) {
   return 0
}
----------------------

如果你从迭代 5 向后计算它的返回值)

5) 0
4) 1
3) 1
2) 1 <== 4) + 5)  = 1 + 0  
1) 2  <== 2) + 3)  = 1 + 1

所以 fib(3) 是 2

【讨论】:

    猜你喜欢
    • 2014-05-19
    • 1970-01-01
    • 1970-01-01
    • 2012-12-29
    • 1970-01-01
    • 2019-12-19
    • 2015-06-05
    • 2014-05-23
    • 2013-08-09
    相关资源
    最近更新 更多