【问题标题】:When does the call stack get set up in Javascript?何时在 Javascript 中设置调用堆栈?
【发布时间】:2021-06-05 19:39:52
【问题描述】:

在尝试解决以下问题:

Generate all combinations of an array of string.
Ex: Input: ['A', 'T', 'C', 'K']
Output: [
  'ATCK', 'ATC', 'ATK',
  'AT',   'ACK', 'AC',
  'AK',   'A',   'TCK',
  'TC',   'TK',  'T',
  'CK',   'C',   'K',
  ''
]


我有以下代码:

function getSpellableWords(arr) {
    const res = [];
    // helper(arr, res, 0, '');
    helper(arr, res, 0, []);
    return res;
}

function helper(arr, res, i, slate) {
  const char = arr[i];
  console.log(i, 'i')
  if(i === arr.length) {
    res.push(slate.join(''));
    return;
  }

  // slate = slate + char;
  slate.push(char)
  console.log(slate, 'slate')
  helper(arr, res, i+1, slate);
  slate.pop()

  helper(arr, res, i+1, slate);
}

getSpellableWords(['A', 'T', 'C', 'K']);

我的问题是:

如果我在代码中删除以下行:

helper(arr, res, i+1, slate);

一旦i 等于5(即array.length),代码将在将slate 推入res 后停止。但是,如果我保留该行,则会设置一个调用堆栈,因此i 将从 1->5 上升,然后逐渐弹出到 4 然后 3 然后回到 4 等等.为什么会这样?

澄清:所以我知道对于每个递归调用,都会生成另一个i 变量。然而,我期待第二次递归调用也再次从 1->4 生成i,但这一次不是线性递增,而是回溯。为什么第一次调用没有回溯,为什么第一次调用只生成第一个结果时,第二次调用会生成其余结果?

【问题讨论】:

  • 因为i 是一个局部变量,helper 的每次新执行都会有自己的i 版本,它可以有不同的值。当递归调用返回并且调用代码继续时,它自己的i 是相关的(再次)。
  • 所以我知道对于每个递归调用,都会生成另一个 i 变量。然而,我期待第二次递归调用也能再次从 1->4 生成i,但这一次不是线性递增,而是回溯。为什么第一次调用没有回溯,为什么第一次调用只生成第一个结果时第二次调用生成其余结果?
  • 你为什么期望递归调用改变i?它永远不会。如果您查看代码,您会发现helper 中没有对i赋值。唯一发生的事情是i+1 被传递给递归调用,在那里它将成为仅存在于该递归执行上下文中的局部变量i 的初始值和唯一值。当该执行终止时,我们回到调用者那里,那里仍然存在未更改的i
  • 你能举个例子吗?所以在这种情况下,在第一个函数调用返回后,i 在 5 处停止。那么接下来会发生什么?现在将1 添加到i 以使其成为6
  • 发表了答案。

标签: javascript arrays recursion backtracking callstack


【解决方案1】:

helper 的每个递归调用确实会在调用堆栈上添加一个级别,因此当递归调用返回到其调用者时,调用代码可以继续使用其自己的本地执行上下文。

helper 的每次执行都有自己的执行上下文,其中包括一个本地 变量i,它只对那个 执行上下文可见。它仅在调用堆栈中的该级别发挥作用。

请注意,helper 代码永远不会更改其 i 变量的值。当它被调用时,它会使用作为第三个参数传递的任何值进行初始化,这是它唯一的值。

您注意到的更改i实际上没有更改。您看到的 i 的每个不同值实际上都是关于一个恰好具有相同名称的不同变量。

这里有一个关于这些i 变量生命周期的小架构,当res 变量的长度为2 时(只是为了不让它太长!):

helper(arr, res, 0, []); // The initial call
    +--------top level helper execution context----+
    | i = 0                                        |
    | ....                                         |
    | slate.push(char)                             |
    | helper(arr, res, i+1, slate);                |
    |      +---nested helper execution context---+ |
    |      | i = 1                               | |
    |      | ....                                | |
    |      | slate.push(char)                    | |
    |      | helper(arr, res, i+1, slate);       | |
    |      |      +--deepest exec. context-----+ | |
    |      |      | i = 2                      | | |
    |      |      |  ...                       | | |
    |      |      | res.push(slate.join(''));  | | |
    |      |      | return;                    | | |
    |      |      +----------------------------+ | |
    |      | // i is still 1                     | |
    |      | slate.pop()                         | |
    |      | helper(arr, res, i+1, slate);       | |
    |      |      +----------------------------+ | |
    |      |      | i = 2                      | | |
    |      |      |  ...                       | | |
    |      |      | res.push(slate.join(''));  | | |
    |      |      | return;                    | | |
    |      |      +----------------------------+ | |
    |      | // i is still 1                     | |
    |      +-------------------------------------+ |
    | // i is still 0                              |
    | slate.pop()                                  |
    | helper(arr, res, i+1, slate);                |
    |      +-------------------------------------+ |
    |      | i = 1                               | |
    |      | ....                                | |
    |      | slate.push(char)                    | |
    |      | helper(arr, res, i+1, slate);       | |
    |      |      +----------------------------+ | |
    |      |      | i = 2                      | | |
    |      |      |  ...                       | | |
    |      |      | res.push(slate.join(''));  | | |
    |      |      | return;                    | | |
    |      |      +----------------------------+ | |
    |      | // i is still 1                     | |
    |      | slate.pop()                         | |
    |      | helper(arr, res, i+1, slate);       | |
    |      |      +----------------------------+ | |
    |      |      | i = 2                      | | |
    |      |      |  ...                       | | |
    |      |      | res.push(slate.join(''));  | | |
    |      |      | return;                    | | |
    |      |      +----------------------------+ | |
    |      | // i is still 1                     | |
    |      +-------------------------------------+ |
    | // i is still 0                              |
    +----------------------------------------------+

所以我们看到,在这个特定的算法中,调用堆栈的大小(即递归树的深度)正好对应于当前执行中变量i的值语境。当函数返回时,调用堆栈的大小减小(即递归深度减小),因此我们到达了一个状态(从堆栈中弹出),其中有另一个i 实例它的值也与调用堆栈的当前 current 大小相匹配。

【讨论】:

  • 您能给点意见吗?
  • 很好的解释!
  • 谢谢。这更清楚。但是,请问为什么需要 2 次递归调用才能建立调用堆栈?如果我们只有 1 个递归调用,那将是一个迭代问题。
  • 确实,这取决于算法要解决的问题。在某些情况下,您需要多个递归调用,而在其他情况下,您只需要一个。当只有一个时,有时可以应用tail recursion的原理,通过迭代的方式重写算法。对于您的代码解决的这个特定问题,还有一种迭代方式来实现结果。但这超出了您的问题范围。
【解决方案2】:

Trincot 给出了关于该功能如何在内部工作的有用的详细回复。我只想指出您可以编写的一个重要的简化:

const getSpellableWords = ([x, ...xs]) => 
  x == undefined
    ? ['']
    : ((ps = getSpellableWords (xs)) => [...ps .map (p => x + p), ...ps]) ()

console .log (
  getSpellableWords (['A', 'T', 'C', 'K'])
)
.as-console-wrapper {max-height: 100% !important; top: 0}

这里我们注意到我们可以创建的单词要么包含第一个字符,要么不包含。我们可以从余数中递归地计算所有单词,然后返回将该单词数组与通过将第一个字符添加到每个单词之前找到的单词组合的结果。当然,如果没有剩余字符,我们的递归就会触底,返回的数组只包含空字符串。

这里有一点句法技巧,递归分支中有一个 IIFE。我们可能更喜欢使用call 辅助函数,它接受一个函数和任何参数,并使用这些参数调用该函数。有时这更清楚:

const call = (fn, ...args) => fn (...args)

const getSpellableWords = ([x, ...xs]) => 
  x == undefined
    ? ['']
    : call (
        (ps) => [...ps .map (p => x + p), ...ps],
        getSpellableWords (xs)
      )

【讨论】:

  • 感谢您的回答。只是想问一下,尽管它更干净,但我认为看代码很难理解。这种编程风格通常在生产中经常使用吗?
  • @Heymanyoulookkindacool:这是一个偏好和经验的问题。我发现这比问题中的代码更容易阅读和推理。我鼓励我的团队朝着这种风格发展,所以我知道至少有一家大公司正在生产这种风格。 ;-)
猜你喜欢
  • 1970-01-01
  • 2021-06-28
  • 1970-01-01
  • 2016-04-14
  • 2012-09-10
  • 2012-06-26
  • 2020-10-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多