【问题标题】:recursion: storage of multiple values in variables递归:在变量中存储多个值
【发布时间】:2020-05-04 19:39:04
【问题描述】:

代码显示了一个递归函数,它接受一个数字,比如 n=5,并返回一个从 n 倒数到 1 的数组,即 [5,4,3,2,1]。

我的困惑就在我们将 n 的数字/值推送到 countArray 之前。我的理解是 countup(n - 1),会生成数字(比如 n=5)5,4,3,2,1...但我不明白它们在哪里/如何存储。我原以为 n 最终会成为它最后定义的值,n=1,或者是一个空白数组。但这不是真的,它们以某种方式都被推入了一个数组,据我所知,在推入之前从未创建/定义过该数组。所以那两条带有 cmets 的线是我需要帮助理解的两条线。

tl;dr: (1) 值 5->1 在被推入数组之前如何存储而不覆盖最终值 1? (2) countArray 在我们推入之前在哪里/如何定义为数组;

function countup(n) {
  if (n < 1) {
    return [];
  } else {
    const countArray = countup(n - 1);  //the storing of 5,4,3,2,1 I don't understand
    countArray.push(n); //I don't understand when countArray was defined as an array
    return countArray;
  }
}
console.log(countup(5)); // [ 1, 2, 3, 4, 5 ]

编辑:这篇文章的标题可能需要更改为数组而不是变量等。

【问题讨论】:

  • return []; 是它被声明为数组的位置。
  • 生成数字然后声明数组,如果事后数字如何进入数组?
  • “事后”是什么意思?逐行跟踪函数的工作原理,您会发现return []; 是定义和返回数组的位置。这发生在 推送之前。
  • 我的意思是 else 语句运行并生成一个 5...4...3...2...1.. 然后是一个数组。如果数组是在所有这些数字之后定义的,我不明白如何将它们放入数组中。
  • 这能回答你的问题吗? Understanding how recursive functions work

标签: javascript recursion


【解决方案1】:

也许添加一些日志记录可能会更清楚。

我们可以添加一个简单的记录器来报告如下的值:

Calling with 5
|   Calling with 4
|   |   Calling with 3
|   |   |   Calling with 2
|   |   |   |   Calling with 1
|   |   |   |   |   Calling with 0
|   |   |   |   |   Returning [] (base case)
|   |   |   |   Pushing 1 to []
|   |   |   |   Returning [1]
|   |   |   Pushing 2 to [1]
|   |   |   Returning [1,2]
|   |   Pushing 3 to [1,2]
|   |   Returning [1,2,3]
|   Pushing 4 to [1,2,3]
|   Returning [1,2,3,4]
Pushing 5 to [1,2,3,4]
Returning [1,2,3,4,5]

所以数组是在基本情况下定义的。然后,当我们备份调用堆栈时,我们添加了它。有其他方法可以做到这一点,但这是一种常见且合理的方法。

你可以在下面的 sn-p 中看到我是如何添加日志的:

const log = (depth, message) => 
  console .log ('|   '.repeat (depth - 1)  + message)


function countup(n, depth = 1) {
  log(depth, `Calling with ${n}`)
  if (n < 1) {
    log(depth, `Returning [] (base case)`)
    return [];
  } else {
    const countArray = countup(n - 1, depth + 1);  //the storing of 5,4,3,2,1 I don't understand
    log(depth, `Pushing ${n} to [${countArray}]`)
    countArray.push(n); //I don't understand when countArray was defined as an array
    log(depth, `Returning [${countArray}]`)
    return countArray;
  }
}

countup(5)
.as-console-wrapper {min-height: 100% !important; top: 0}

更新

也许这个结果会更清楚:

/ Calling with 5
|   / Calling with 4
|   |   / Calling with 3
|   |   |   / Calling with 2
|   |   |   |   / Calling with 1
|   |   |   |   |   / Calling with 0
|   |   |   |   |   \ Returning [] (base case)
|   |   |   |   | Pushing 1 to []
|   |   |   |   \ Returning [1]
|   |   |   | Pushing 2 to [1]
|   |   |   \ Returning [1,2]
|   |   | Pushing 3 to [1,2]
|   |   \ Returning [1,2,3]
|   | Pushing 4 to [1,2,3]
|   \ Returning [1,2,3,4]
| Pushing 5 to [1,2,3,4]
\ Returning [1,2,3,4,5]

仅涉及对日志记录语句的微小更改:

const log = (depth, message) => 
  console .log ('|   '.repeat (depth - 1)  + message)
  
function countup(n, depth = 1) {
  log(depth, `/ Calling with ${n}`)
  if (n < 1) {
    log(depth, `\\ Returning [] (base case)`)
    return [];
  } else {
    const countArray = countup(n - 1, depth + 1);  //the storing of 5,4,3,2,1 I don't understand
    log(depth, `| Pushing ${n} to [${countArray}]`)
    countArray.push(n); //I don't understand when countArray was defined as an array
    log(depth, `\\ Returning [${countArray}]`)
    return countArray;
  }
}

countup(5)
.as-console-wrapper {min-height: 100% !important; top: 0}

【讨论】:

  • 这是对 imo 的精彩解释
  • 好吧,这需要一段时间,但我现在知道它是如何工作的了。这一点,通过@CristianTraìna 调试器查看,并像"Context: {n=1 at line 6}" 一样写出每个子步骤,这对我来说是这样做的。
  • 美丽的答案,斯科特。 Array(depth).fill('| ').join('').slice(0, -4)'| '.repeat(depth - 1) 一样吗?
  • @Thankyou:是的。我从没想过.repeat。更新了答案。
【解决方案2】:

每个至少结束的递归函数都会有一个停止条件。

对于你的功能是

if (n < 1) {
 return [];
}

递归函数的另一部分是实际的递归。

这发生在这里

const countArray = countup(n - 1);

您正在使用少一个n 调用该函数。

您将点击该分支并触发递归,直到您到达&lt;1。此时数组被创建并被返回。

之后,您可以将值推送到该数组中。

return countArray;很重要,这样数组就被推送到调用函数了。

您最有可能缺少的是,当调用一个函数时,调用函数会等待它结束,然后继续执行。

如果您尝试映射堆栈并将所有调用映射到函数,通常您会理解递归函数。

【讨论】:

    【解决方案3】:

    我知道这应该是评论,但我无法在评论部分添加代码)。

    我认为了解数组的创建位置的最佳方法是自己遵循流程。它会让你比所有可以解释的词都更明白。

    只需执行以下步骤:

    1. 在浏览器中打开控制台。按F12,然后点击控制台标签
    2. 使用debugger 语句粘贴代码:

    -

    function countup(n) {
      debugger;
      if (n < 1) {
        return [];
      } else {
        const countArray = countup(n - 1);  //the storing of 5,4,3,2,1 I don't understand
        countArray.push(n); //I don't understand when countArray was defined as an array
        return countArray;
      }
    }
    console.log(countup(5)); // [ 1, 2, 3, 4, 5 ]
    
    1. 使用step instep over 按钮
    2. 玩得开心!

    【讨论】:

      【解决方案4】:

      此时

      const countArray = countup(n - 1);
      

      函数“等待”并引发对自身的调用,它将仅在 countup(0) 处的停止条件 if (n &lt; 1) 处停止,这将返回一个空数组并返回到前面的 countup(1) 调用。此时 countArray 获取 countup(0) 返回的值,即 [] 并将其声明为数组,这也使得未来的push 调用有效。

      变量同时存储在内存中。调用函数时,其参数、局部变量和返回地址都存储在内存堆栈中。

      您可能想了解更多关于递归和调用堆栈的信息。

      【讨论】:

        【解决方案5】:

        TL;DR

        return [];中定义。

        每次进入函数时参数(n)递减,达到0时创建数组。


        比方说,你打电话给countup(2)

        这将到达调用countup(1) 的else。该过程重复并调用countup(0)

        这不会进入else 分支,而是创建并返回一个空数组。

        调用方法countup(1)向它添加1(并返回结果),它的调用者countup(2)添加2

        我将尝试可视化递归:

        countup(2)
          2<1 --> false --> else
          countArray=countup(1)
            1<1 --> false --> else
              countArray=countup(0)
                0<1 --> true --> if
                return []
              push 1 --> [1]
              return countArray --> [1]
          push 1 -->[1,2]
          return countArray -->[1,2]
        

        【讨论】:

          猜你喜欢
          • 2013-07-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-02-16
          • 1970-01-01
          相关资源
          最近更新 更多