这个递归是如何工作的?

IT技术 javascript algorithm recursion
2021-02-03 06:38:29

这是 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)
6个回答

该函数运行一个相当简单的带有回溯的蛮力搜索:在每个调用级别,它尝试添加到数字,看看从结果数字开始是否能让你达到目标。如果是,则返回结果;否则,该数字乘以,并从该新数字继续搜索目标。随着递归的进行,产生数字的表达式的文本表示被传递到下一个调用级别。53

搜索过程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. 是这样吗?
2021-03-23 06:38:29
感谢您分解步骤,这对我有帮助。
2021-03-26 06:38:29
@amnotiam 你是对的,我以某种方式开始times three而不是plus five. 我更改了顺序以匹配正在发生的事情。
2021-04-05 06:38:29

你只需要创建调用树来解决这个问题:

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
你是如何生成这个树表示的?它非常好。
2021-03-22 06:38:29
...oop,除了您似乎忽略了||运营商的短路问题,因此return "(((1 * 3) + 5) * 3)"应该删除之后的任何内容
2021-03-24 06:38:29
@amnotiam 你是对的,但是当我意识到这一点时,我已经太遥不可及了 :) ...
2021-03-30 06:38:29

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

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

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

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

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

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

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

简单来说,find(start,goal)只要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");
}

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

这有帮助吗?