【问题标题】:How does this recursion work?这个递归是如何工作的?
【发布时间】:2013-03-20 16:26:21
【问题描述】:

这是一个来自 Eloquent Javascript 的示例:

从数字 1 开始,反复加 5 或 乘以 3,可以产生无限数量的新数字。 你将如何编写一个函数,给定一个数字,试图找到一个 产生该数字的加法和乘法序列?

我无法理解这里的递归是如何工作的,想知道是否有人可以多次写出 find 是如何被调用的或其他一些解释。

function findSequence(goal) {
  function find(start, history) {
    if (start == goal)
      return history;
    else if (start > goal)
      return null;
    else
      return find(start + 5, "(" + history + " + 5)") ||
             find(start * 3, "(" + history + " * 3)");
  }
  return find(1, "1");
}

console.log(findSequence(24)); // => (((1 * 3) + 5) * 3)

【问题讨论】:

标签: javascript algorithm recursion


【解决方案1】:

此函数从 1 开始,然后尝试将其加 5 或将其乘以 3。如果等于目标,则函数终止并打印出找到的表达式。如果没有,它会使用该级别的值递归调用自身,直到找到匹配项或直到值变得过高。

这有帮助吗?

【讨论】:

    【解决方案2】:

    有人可以写出几次 find 是如何被调用的。

    给你:

    find(1, "1") -> find(3, "(1 * 3)")
                 -> find(8, "((1 * 3) + 5)")
                 -> find(24, "(((1 * 3) + 5) * 3)")
    

    【讨论】:

      【解决方案3】:

      简单来说,只要没有达到goal 的值,find(start,goal) 就会被递归调用。在每次调用中,当前数字将乘以 3 或增加 5。history 变量存储执行操作的字符串。当前操作在每次迭代中都附加到该字符串。

      解释:

      function findSequence(goal) {
      
        // This inner function will be called recursively.
        // 'history' is a string with the current operations "stack"
        function find(start, history) {
          if (start == goal)           // if goal is achieved, simply return the result
                                       // ending the recursion
            return history;
          else if (start > goal)       // return null to end the recursion
            return null;
          else
            // One of the 'find' calls can return null - using ||
            // ensures we'll get the right value.
            // Null will be returned if 'start+5' or 'start*3' is
            // greater than our 'goal' (24 in your example).
            // The following line is where a recursion happens.
            return find(start + 5, "(" + history + " + 5)") ||
                   find(start * 3, "(" + history + " * 3)");
        }
      
        // Start with '1'
        return find(1, "1");
      }
      

      【讨论】:

        【解决方案4】:

        像二叉树一样想象加 5 和乘以 3 的无限组合。顶部是最容易计算的数字,1(实际上是“无需步骤”的答案)。下一层左侧是1+5,右侧是1*3。在每个级别,方程都会解析为一个新值(具有更复杂的历史)。这个等式在该树中导航,直到找到一个等于goal 的节点。如果树的分支上的节点产生的值大于您的目标,则它返回 null (从而停止该分支的进一步回避,这是因为这两个操作都只会增加该值,因此一旦您最终大于没有点继续查找),如果一个节点的值等于目标,那么它作为答案返回(连同它用来到达那里的路径)。当值小于时,两条路径都可能保留答案,因此它在每个路径上调用 find。这就是 JavaScript 的“真实”布尔逻辑的用武之地。通过使用 || (OR) 运算符,JavaScript 将首先查看树的 +5 一侧。如果返回 0 或 null,则将执行另一个调用(向下查看 *3)。如果任何返回的计算结果为非 false 值,则它将返回堆栈并结束搜索。

        【讨论】:

          【解决方案5】:

          该函数使用backtracking 运行一个相当简单的brute force search:在每个调用级别它尝试将5 添加到数字,并查看是否从结果数字开始让您到达目标。如果是,则返回结果;否则,该数字将乘以3,然后从该新数字继续搜索目标。随着递归的进行,产生数字的表达式的文本表示被传递到下一个调用级别。

          14 的搜索过程如下:

          (1,  "1")
          (5,  "1+5")
          (10, "(1+5)+5")
          (15, "((1+5)+5)+5") <<= Fail
          (30, "((1+5)+5)*3") <<= Fail
          (15, "(1+5)*3") <<= Fail
          (3,  "1*3")
          (8,  "(1*3)+5")
          (13, "((1*3)+5)+5")
          (18, "(((1*3)+5)+5)+5") <<= Fail
          (39, "(((1*3)+5)+5)*3") <<= Fail
          (24,  "((1*3)+5)*3") <<= Fail
          (9, "(1*3)*3")
          (14, "((1*3)*3)+5) <<= Success!
          

          【讨论】:

          • 感谢您分解步骤,这对我有帮助。
          • 您似乎在暗示这是在搜索14 的结果时所采取的步骤的表示。是这样吗?
          • @amnotiam 你说得对,我不知何故从times three 开始,而不是plus five。我更改了顺序以匹配正在发生的事情。
          【解决方案6】:

          find 的主体有三个退出路径,两个对应于停止递归的条件,一个对应于递归:

          if (start == goal)
            return history; // stop recursion: solution found
          else if (start > goal)
            return null;    // stop recursion: solution impossible
          else
            // ...
          

          第三条路径实际上是一个“分支”,因为它递归了两次(一次尝试加法,一次尝试乘法):

            return find(start + 5, "(" + history + " + 5)") ||
                   find(start * 3, "(" + history + " * 3)");
          

          那么这里发生了什么?

          首先,请注意,这两个find 调用中的每一个都将评估为非空字符串(操作历史记录)或null。由于非空字符串是“真”值,而null 是“假”值,我们通过将它们与|| 运算符连接来利用这一点;如果它为真,则此运算符对它的第一个操作数求值,否则对第二个操作数求值。

          这意味着将首先评估第一个递归分支 (+5)。如果有一个以加 5 开始并达到目标的操作序列,则返回该序列的描述。否则,如果有一个以乘以 3 开始并达到目标的序列,则将返回该历史的描述。

          如果无法达到目标,则返回值将是第二个分支生成的null

          【讨论】:

            【解决方案7】:

            了解这一点的最佳方法是在 JavaScript 调试器中跟踪代码。

            你以前用过调试器吗?这真的很有趣,很有启发性,也很容易。

            只需在您希望代码停止的位置添加debugger; 语句。一个好地方就在你打电话给findSequence()之前:

            debugger;
            console.log(findSequence(24));
            

            现在在打开开发者工具的情况下在 Chrome 中加载您的页面。您的代码将停在debugger; 行。找到让您进入代码的按钮(在 Watch Expressions 面板的右上方)。单击该按钮进入findSequence() 呼叫。每次点击它都会进入下一行代码,包括进入每个递归调用。

            当代码停止时,您可以将鼠标悬停在任何变量上以查看它,或查看右侧面板中的变量。还有一个调用堆栈可以准确地显示你在递归调用中的位置。

            我相信有人可以向您解释递归,但如果您通过在调试器中逐步执行代码来实际体验它,您会学到更多。

            【讨论】:

              【解决方案8】:

              如果你去掉漂亮的打印内容,代码会更容易阅读:

              function findSequence(goal) {
                  function find(start) {
                      if (start == goal) {
                          return true;
                      } else if (start > goal) {
                          return false;
                      } else {
                          return find(start + 5) || find(start * 3);
                      }
                  }
              
                  return find(1);
              }
              

              外部函数findSequence 动态创建一个名为find 函数,其中goal 取自父函数的范围。为了清楚起见,您可以像这样重写它:

              function findSequence(start, goal) {
                  if (start == goal) {
                      return true;
                  } else if (start > goal) {
                      return false;
                  } else {
                      return findSequence(start + 5, goal) || findSequence(start * 3, goal);
                  }
              }
              

              现在,您可以更清楚地看到发生了什么。递归步骤在最后的return 语句中,它在每一步都尝试start + 5start * 3,并选择最终返回true 的分支。

              手动遵循findSequence(1, 23)的逻辑,你就会明白它是如何工作的。

              【讨论】:

                【解决方案9】:

                让我们留下历史参数,我们稍后再谈。

                递归扩展到所有可能的操作。 它以 1 的值作为 start 开头。

                1. 我们首先检查我们是否到达了目的地:goal,如果到达则返回true,这意味着我们走的路是正确的。

                2. 其次,我们问 - 我们是否越界 (goal)?如果我们这样做了,我们应该返回false,因为这条路径对我们没有帮助。

                3. 否则,让我们尝试两种可能性(我们使用 OR,因为我们至少需要一种):

                  • 调用相同的函数,但将start设置为start + 5
                  • 调用相同的函数,但将start设置为start * 3

                历史变量保持我们采取的步骤。因此,如果函数调用识别出start == goal,它就会返回它。

                【讨论】:

                  【解决方案10】:

                  goal 是你的目标,它设置为 24

                  goal == 24
                  

                  现在我们有了这个内部函数find(),它检查start是否等于24;它不是。 它还会检查 start 是否大于 24 这也不正确,

                  find(1 "1")
                  1 == 24 //false
                  1 > 24 //false
                  

                  所以它会再次调用 find 的 else 语句,这是来自 else if() 的空值的来源。如果返回为空,则它调用 ||直到最终找到正确答案为止。

                  return find(6, "(1 + 5)")
                         find(11, "((1 + 5) + 5)")
                         find(16, "(((1 + 5) + 5) +5)")
                         find(21, "((((1+5) + 5) + 5) +5)")
                         //next one returns null!
                         //tries * by 3 on 21, 16, and 11 all return null 
                  

                  所以它切换到 ||

                  return find(3, "(1 * 3)")
                         find(8, "((1 * 3) +5)")
                         //some calls down +5 path but that returns null
                         find(24, "(((1 * 3) + 5) * 3)")
                  

                  终于!我们有一个真正的回报,我们已经记录了我们在历史变量中所走的路径。

                  【讨论】:

                    【解决方案11】:

                    您只需创建调用树即可解决此问题:

                    findSequence(24)
                        find(1, "1")
                           find(1 + 5, "(1 + 5)")
                               find(6 + 5, "((1 + 5) + 5)")
                                   find(11 + 5, "(((1 + 5) + 5) + 5)"
                                       find(16 + 5, "((((1 + 5) + 5) + 5) + 5)"
                                           find(21 + 5, "(((((1 + 5) + 5) + 5) + 5) + 5)"
                                              start > goal: return null
                                           find(21 * 3, "(((((1 + 5) + 5) + 5) + 5) + 5)" 
                                              start > goal: return null
                                       find(16 * 3, "((((1 + 5) + 5) + 5) * 3)"
                                           start > goal: return null
                                   find(11 * 3, "(((1 + 5) + 5) * 3)"
                                       start > goal: return null
                               find(6 * 3, "((1 + 5) * 3)")
                                   find(18 + 5, "(((1 + 5) * 3) + 5)")
                                       find(23 + 5, "((((1 + 5) * 3) + 5) + 5)")
                                           start > goal: return null
                                       find(23 * 3, "((((1 + 5) * 3) + 5) * 3)")
                                           start > goal: return null
                                   find(18 * 3, "(((1 + 5) * 3) * 3)")
                                       start > goal: return null
                           find(1 * 3, "(1 * 3)") 
                               find(3 + 5, "((1 * 3) + 5)")
                                   find(8 + 5, "(((1 * 3) + 5) + 5)")
                                       find(13 + 5, "((((1 * 3) + 5) + 5) + 5)")
                                           find(18 + 5, "(((((1 * 3) + 5) + 5) + 5) + 5)")
                                               find(23 + 5, "((((((1 * 3) + 5) + 5) + 5) + 5) + 5)")
                                                   start > goal: return null
                                               find(23 + 5, "((((((1 * 3) + 5) + 5) + 5) + 5) + 5)")
                                                   start > goal: return null
                                           find(18 * 3, "(((((1 * 3) + 5) + 5) + 5) * 3)")
                                               start > goal: return null
                                       find(13 * 3, "((((1 * 3) + 5) + 5) * 3)")
                                           start > goal: return null
                                   find(8 * 3, "(((1 * 3) + 5) * 3)")
                                       return "(((1 * 3) + 5) * 3)"
                               find(3 * 3, "((1 * 3) * 3)")
                                   find(9 + 5, "(((1 * 3) * 3) + 5)")
                                      find(14 + 5, "((((1 * 3) * 3) + 5) + 5)")
                                          find(19 + 5, "(((((1 * 3) * 3) + 5) +5) + 5)")
                                             return "(((((1 * 3) * 3) + 5) +5) + 5)"
                                          find(19 * 3, "((((1 * 3) * 3) + 5) *3)")
                                              start > goal: return null
                                   find(9 * 3, "(((1 * 3) * 3) * 3)")
                                        start > goal: return null
                    

                    【讨论】:

                    • ...哎呀,除非您似乎忽略了考虑 || 运算符的短路,所以应该删除 return "(((1 * 3) + 5) * 3)" 之后的任何内容。
                    • @amnotiam 你是对的,但是当我意识到这一点时,我已经走得太远了,无法删除:) ...
                    • 您是如何生成此树表示的?太棒了。
                    猜你喜欢
                    • 2016-11-01
                    • 1970-01-01
                    • 2019-01-30
                    • 1970-01-01
                    • 2011-01-25
                    • 2021-01-27
                    • 1970-01-01
                    • 2011-03-29
                    • 2021-04-12
                    相关资源
                    最近更新 更多