这种递归如何工作?

时间:2013-03-29 22:13:13

标签: javascript algorithm recursion

这是来自Eloquent Javascript的示例:

  

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

我无法理解递归在这里是如何工作的,想知道是否有人可以写出几次如何调用查找或其他解释。

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)

11 个答案:

答案 0 :(得分:7)

该函数使用brute force search运行一个相当简单的 backtracking :在每个调用级别,它会尝试将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!

答案 1 :(得分:3)

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

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

答案 2 :(得分:2)

简单来说,只要尚未达到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");
}

答案 3 :(得分:2)

最好的方法是跟踪JavaScript调试器中的代码。

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

只需在希望代码停止的位置添加debugger;语句即可。在您致电findSequence()之前,一个好地方就是:

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

现在,在打开开发人员工具的情况下将您的页面加载到Chrome中。您的代码将停在该debugger;行。找到允许您单步执行代码的按钮(在Watch Expressions面板的右上方)。点击该按钮即可进入findSequence()来电。每次单击它时,它将进入下一行代码,包括进入每个递归调用。

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

我确信有人可以向您解释递归,但如果您通过调试器中的代码实际体验它,您将学到更多。

答案 4 :(得分:1)

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

这有帮助吗?

答案 5 :(得分:1)

  

有人可以写出几次如何调用发现。

你走了:

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

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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。由于非空字符串是“truthy”值而null是“虚假”值,因此我们通过将它们与||运算符连接来利用它;如果它是真的,则运算符求值为第一个操作数,否则求值为第二个操作数。

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

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

答案 8 :(得分:1)

如果你摆脱了漂亮的印刷品,那么代码就更容易理解了:

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 new 函数,其中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)

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

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

  1. 我们首先检查我们是否到达目的地:goal,如果我们这样做 - 返回true,这意味着我们采取的路径是正确的。

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

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

    • 调用相同的函数,但将start设置为start + 5
    • 调用相同的函数,但将start设置为start * 3
  4. 历史记录变量保留我们采取的步骤。因此,如果函数调用标识start == goal它返回它。

答案 10 :(得分:1)

goal是您的目标,它已设置为24

goal == 24

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

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

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

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)")

最后!我们有一个真正的回报,我们记录了我们在历史变量中所采用的路径。