无法理解 freeCodeCamp 中的 JS 递归倒计时函数

Fer*_*osd 3 javascript arrays recursion loops

所以,我已经理解了JavaScript 中递归的概念(有一个函数可以循环自身,直到达到基本条件,此时它停止并返回最终结果),但我有一点当我将其应用到尝试应用它来创建数组的实际语法时,我感到头疼。

\n

让我们使用freeCodeCamp 的倒计时练习作为示例。这是正确的结果:

\n
function countdown(n) {\n  if (n < 1) {\n    return [];\n  } else {\n    const arr = countdown(n - 1);\n    arr.unshift(n);\n    return arr;\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这里的想法显然是有一个函数从给定的n倒数到 1。我理解这个想法,但我有几个问题:

\n

1 -为什么将数组声明为常量分配给递归调用?数组不应该更好地在外部声明为变量(因为它会更改值)\xe2\x80\x93 并且其当前位置不意味着每次发生递归时都会声明(并覆盖)它?

\n

2 -我得到了当前值的不变,但为什么它是在递归调用之后声明的?难道它不应该放置在之前(上面一行)以便在函数循环本身之前取消移位值吗?

\n

3 -同样,为什么在 else 循环中返回完整的最终数组?难道不应该更好地在外部返回,或者在上面的基本条件上返回,以免每次函数循环时都返回吗?

\n

事实上,我的理解是,当n达到 0 时,循环应该返回一个数组,而不是通过递归创建的完整数组。

\n

~~~

\n

最终编辑/理解:感谢每一个答案!因此,如果我目前的理解是正确的,递归不会循环该函数,正如我最初认为 \xe2\x80\x93 相反,它有点“打开” “链式函数”的调用该调用向下移动,直到找到并解析基本情况

\n

“基本”情况或函数得到解决时,链会“向上”移动,也就是说,解决上面的每个递归步骤取消每个 n 值的移位,并“向上”返回结果数组,直到达到初始 n 值,此时返回最终数组,并且退出该函数。

\n

我想我现在(有点)明白了!实际上编写递归函数并不容易,但我更好地掌握了这个过程的实际发生方式。

\n

LMD*_*LMD 5

\n

为什么将数组声明为常量并分配给递归调用?

\n
\n

它不是。该数组是可变的。每次调用此函数都会返回一个新数组。没有数组被“覆盖”。const在 JavaScript 中仅用于声明一个常量变量,该常量变量具有词法作用域(即该变量arr在 后不再存在return arr)并且是常量(不能被变异/赋值)。

\n
\n

我得到了当前值的不变,但为什么它是在递归调用之后声明的?难道它不应该放置在之前(上面一行)以便在函数循环本身之前取消移位值吗?

\n
\n

您不能将其放置在上面,因为arr首先需要递归调用来获取。n-1从到倒计时后1,您必须n在第一个位置添加前缀,这就是unshift执行的操作。

\n
\n

同样,为什么在 else 循环中返回完整的最终数组?难道不应该更好地在外部返回,或者在上面的基本条件下返回,以免每次函数循环时都返回吗?

\n
\n

不存在“else循环”这样的东西。在那里返回数组的原因是您想countdown(n)使用 array 返回它刚刚生成的数组countdown(n-1)

\n
\n

事实上,我的理解是,当 n 达到 0 时,循环应该返回一个空数组,而不是通过递归创建的完整数组。

\n
\n

正是如此if (n < 1) return [];

\n
\n

也就是说,对于手头的任务,给出的递归实现非常不理想。首先,你不需要 ifelse无论如何return都会退出该函数:

\n
function countdown(n) {\n  if (n < 1) return [];\n\n  const arr = countdown(n - 1);\n  arr.unshift(n);\n  return arr;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

其次,作为一个简单的循环,这是最具可读性(和性能)的for

\n
function countdown(n) {\n  const arr = Array(n)\n  for (let i = 0; i < n; i++) arr[i] = n - i;\n  return arr;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

(奖励:可以使用构造函数利用提前知道的数组大小Array,以防止 JS 在推送元素时必须调整数组大小)。

\n

第三,当前的实现具有O(n\xc2\xb2) 的二次时间复杂度,因为arr.unshift是一个线性时间操作,将n时间应用于长度0为 的数组n-1

\n

当您需要堆栈时(深度优先遍历中的 fE),递归非常有用。创建倒计时不需要堆栈。这不是使用而是滥用递归来实现高度次优的算法。如果您 - 无论出于何种原因(也许您被迫使用没有循环,只有递归的纯函数式语言?) - 想要不惜for一切代价用递归调用替换完美的精细循环,只需使用尾递归和词法作用域(关闭):

\n
function countdown(n) {\n  const arr = [];\n  function loop(n) {\n    if (n < 1) return;\n    arr.push(n);\n    loop(n-1);\n  }\n  loop(n);\n  return arr;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

或者如果您不喜欢闭包,那么可选arr参数怎么样?这允许扭转递归,因为调用者现在拥有被arr调用者之前的号码,因此可以在后续递归调用执行相同操作之前附加其编号:

\n
function countdown(n, arr = []) {\n  if (n < 1) return arr;\n  arr.push(n);\n  countdown(n - 1, arr);\n  return arr;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

结果相同,但(IMO)更干净,最重要的是,代码速度明显更快。尝试运行所有这些函数n = 1e6,您会发现 freeCodeCamp 函数无法在合理的时间范围内完成,而其他函数几乎立即完成。

\n