【问题标题】:How does this complex recursive code work?这个复杂的递归代码是如何工作的?
【发布时间】:2021-04-12 12:22:36
【问题描述】:

我试图理解这种递归。我知道递归在阶乘函数中是如何工作的,但是当涉及到像这样的复杂递归时,我感到很困惑。对我来说最令人困惑的部分是这段代码

str.split('').map( (char, i) => 
    permutations( str.substr(0, i) + str.substr(i + 1) )map( p => char + p))

首先,使用"abc",例如,它将拆分为["a","b","c"]并通过map函数,然后通过第二个map函数将每个返回包装为abc,分别。但是,我在递归部分很困惑。

我认为"a" 中的第一个递归值为str"abc" 将返回"bc",第二次递归str 值为"bc" 将返回"c",依此类推。

但是当我刚刚运行这段代码以查看清晰的递归时,它会返回

[ [ [ 'c' ], [ 'b' ] ], [ [ 'c' ], [ 'a' ] ], [ [ 'b' ], [ 'a' ] ] ]

这让我最困惑。我只是看不到这个递归如何返回这些值。谁能更详细地了解它的工作原理,例如逐步说明您的思维过程?

我是一个视觉学习者。感谢您的帮助。

function permutations(str) {
 return (str.length <= 1) ? [str] :
      // Array.from(new Set(
        str.split('')
              .map( (char, i) => 
                     permutations( str.substr(0, i) + str.substr(i + 1))
                           .map( p => char + p))
            //  .reduce( (r, x) => r.concat(x), [])
        //  ));
}

permutations('abc')

【问题讨论】:

  • 至于您的编辑,您确定这就是您所得到的吗?当我运行代码时,我得到[[["abc", "acb"], ["bac", "bca"], ["cab", "cba"]]。但是,要使这项工作正常进行,您需要取消注释 reduce 调用。当我这样做时,我会得到正确的["abc", "acb", "bac", "bca", "cab", "cba"]
  • 您显示的输出看起来好像还注释掉了内部 .map 调用。如果你真的在问别人的代码是如何工作的,你真的需要保留那个代码。如果您想知道为什么您自己的尝试不起作用,请改为提出该问题。
  • 这实际上是对permutations 的非常不寻常的定义,我碰巧从未见过。 :) 在伪代码中,通常看到的简单-递归定义是perms [x, ...xs] = [ [...as, x, ...bs] | p &lt;- perms xs, (as, bs) &lt;- splits p]。但是这个是perms2 xs = [ [x, ...p] | (as, [x, ...bs]) &lt;- splits xs, p &lt;- perms2 [...as, ...bs]](带有列表理解和模式;没有空列表案例;使用splits 的“自然”定义,它构建了将列表分成两部分的所有可能性的列表)。有趣的。并且不是“简单”递归的。 :)
  • (或者,要以明显的方式实现一些命名函数,perms [x, ...rest] = [ i | p &lt;- perms rest, i &lt;- inserts x p] = flatMap (inserts x) (perms rest) 和这个版本,perms2 xs = [ [x, ...p] | (x, rest) &lt;- picks xs, p &lt;- perms2 rest]...那里有一定的二元性。)

标签: javascript recursion permutation


【解决方案1】:

我更喜欢分析和创建递归解决方案的一种方法是像数学归纳法一样工作1

诀窍是显示该函数为我们的基本情况返回正确的值,然后显示如果它为我们的简单情况返回正确的值,它也会为我们当前的情况返回正确的值。然后我们知道它适用于所有值,只要每个递归调用都是针对一些更简单的情况并最终导致基本情况。

所以看看你的功能。我已对其进行了重新格式化以使讨论更容易,并且我已恢复您已注释掉的 reduce 呼叫。事实证明,正确执行此操作是必要的(尽管我们将在下面讨论更现代的替代方案。)您还注释掉了 Array .from (new Set( ... )) 包装器,它用于在您的字符串包含重复字符的情况下删除重复项。没有这个,"aba" 返回["aba", "aab", "baa", "baa", "aab", "aba"]。有了它,我们得到["aba", "aab", "baa"],这更有意义。但这与我们的递归问题是分开的。

清理后的函数如下所示:

function permutations (str) {
  return (str .length <= 1) 
    ? [str] 
    : str 
        .split ('')
        .map ((char, i) => 
          permutations (str .substr (0, i) + str.substr (i + 1))
            .map (p => char + p)
        ) 
        .reduce ((r, x) => r .concat (x), [])
}

permutations('abc')

我们的基本情况非​​常简单,str.length &lt;= 1。在这种情况下,我们产生[str]。这只有两种可能:字符串为空,我们返回[''],或者字符串只有一个字符,比如'x',我们返回['x']。这些显然是正确的,所以我们继续进行递归调用。

假设我们通过了'abc'splitmap 调用将其转换为等效的:

[
  permutations ('bc') .map (p => 'a' + p), 
  permutations ('ac') .map (p => 'b' + p),
  permutations ('ab') .map (p => 'c' + p),
]

但我们假设我们的递归适用于较小的字符串'bc''ac''ab'。这意味着permutations('bc') 将产生['bc', 'cb'],其他类似,所以这相当于

[
  ['bc', 'cb'] .map (p => 'a' + p), 
  ['ac', 'ca'] .map (p => 'b' + p),
  ['ab', 'ba'] .map (p => 'c' + p),
]

这是

[
  ['abc', 'acb']
  ['bac', 'bca']
  ['cab', 'cba']
]

现在我们调用reduce,依次将每个数组连接到前一个结果上,从[]开始,得到

['abc', 'acb', 'bac', 'bca', 'cab', 'cba']

有一种更简洁的方法可以做到这一点。我们可以用一个flatMap 调用替换map 调用,然后是这个reduce 调用,如下所示:

function permutations (str) {
  return (str .length <= 1) 
    ? [str] 
    : str 
        .split ('')
        .flatMap ((char, i) => 
          permutations (str .substr (0, i) + str.substr (i + 1))
            .map (p => char + p)
        ) 
}

无论如何,我们已经展示了我们的归纳技巧。通过假设这适用于更简单的情况,我们表明它适用于当前情况。 (不,我们并没有严格地做到这一点,只是通过示例,但是用某种数学严谨性来证明这一点并不难。)当我们将它与它适用于基本情况的证明结合起来时,我们证明它适用于所有情况。这取决于我们的递归调用在某种程度上更简单,最终导致基本情况。在这里,传递给递归调用的字符串比我们提供的字符串短一个字符,因此我们知道最终我们将达到str .length &lt;= 1 条件。因此我们知道它是有效的。

如果您重新添加 Array .from (new Set ( ... )) 包装器,这也适用于具有重复字符的情况。


1 您可能遇到过归纳,也可能没有遇到过归纳,您可能记得也可能不记得,但从本质上讲,它非常简单。这是一个非常简单的数学归纳论证:

我们将证明1 + 2 + 3 + ... + n == n * (n + 1) / 2,对于所有正整数,n

首先,当n1 时,我们可以很容易地看出这是真的: 1 = 1 * (1 + 1) / 2

接下来我们假设该陈述对于n以下的所有整数都是正确的。

我们证明n 是这样的:

1 + 2 + 3 + ... + n1 + 2 + 3 + ... + (n - 1) + n 相同,即(1 + 2 + 3 + ... (n - 1)) + n。但是我们知道该语句对于n - 1 是正确的(因为我们假设它对于低于n 的所有整数都是正确的),所以1 + 2 + 3 + ... + (n - 1) 是,通过用n - 1 代替上面表达式中的n,等于(n - 1) * ((n - 1) + 1) / 2,简化为(n - 1) * n / 2。所以现在我们更大的表达式((1 + 2 + 3 + ... (n - 1)) + n((n - 1) * n / 2) + n 相同,我们可以将其简化为(n^2 - n) / 2 + n,然后简化为(n^2 - n + (2 * n)) / 2(n^2 + n) / 2。这会影响n * (n + 1) / 2

因此,通过假设小于n 的所有内容都为真,我们证明n 也为真。再加上当n1 时为真,归纳原理表明它对所有正整数n 都为真。

您可能已经看到归纳的表述略有不同:如果 (a)1 成立,(b) 对于 n - 1 成立意味着它是真的对于n,然后(c)对于所有正整数n 都是正确的。 (这里的区别是我们不需要假设它对低于n 的所有整数都为真,仅适用于n - 1。)证明这两个模型的等价性很容易。而everything below 公式通常可以更方便地类比递归问题。

【讨论】:

  • 在归纳证明的结论性示例中,您只使用了n-1 为真的假设,而不是n 下的所有(这就足够了)。事实上,普通的归纳原则表明,对于n-1 =&gt; n 归纳步骤(n 下的 all 称为"complete (or strong) induction")。
  • @WillNess:好吧,也许我没有说得足够清楚,因为对于低于n 的所有整数都是如此(这是我所说的假设),那么对于n - 1 也是如此。当然,这就是我需要用于证明的全部内容,但我故意使用强归纳,因为这通常是递归类比所必需的。当然,强归纳和弱归纳在它们可以显示的内容上是完全等价的,这就是我在该脚注的最后一段中试图说明的内容。
  • @WillNess:添加了括号注释,因此句子现在变为“但我们知道该陈述对于n - 1 是正确的(因为我们假设它对于n 以下的所有整数都是正确的)”这有帮助吗?
  • 如果是故意的,我不应该插手。:) 只是有时我们需要进行多次递归调用,有时只需一个就足够了。后者是前者的子案例,是的,但区别仍然有用。 (我刚刚用谷歌搜索“组构对应于强归纳吗?”它给了我一些 PDF 引用“门德勒风格的组构(对应于强归纳)......”所以答案似乎是肯定的。简单递归是一种变态 AFAICT。我想它更简单,并且可以转换为循环......)
  • 我只知道它的名字,显然它是递归,它可以访问它的整个历史(自我?)。这就是我在那个谷歌查询中做出这个假设的原因。至于“更简单的情况”,“更简单的情况”就更简单了! :)(如果我能找到解决问题的最简单方法,我总是更喜欢。)
【解决方案2】:

让我们检查一下permutations('abc')

'abc' 转换为['a','b','c'] 用于映射

地图

一个

首先,char='a',i=0。 请注意,permutations(str.substr(0, i) + str.substr(i + 1)) 的意思是“获取除我正在查看的字符之外的所有字符的排列。在这种情况下,这意味着permutations('bc')。假设这给出了正确的输出['bc','cb'],因为是归纳的假设

.map(p =&gt; char + p) 然后告诉我们将我们正在查看的字符 ('a') 添加到每个较小的排列中。这会产生['abc',acb']

b

遵循相同的逻辑,char='b',i=1'permutations('ac') == ['ac','ca']。最终输出为['bac','bca']

c

遵循相同的逻辑,char='c',i=2'permutations('ab') == ['ab','ba']。最终输出为['cab','cba']

因此函数的整体输出将是[['abc','acb'],['bac','bca'],['cab','cba']]...

【讨论】:

  • 请注意(注释掉的)reduce 调用是必要的。如果你用'abcd' 试试这个,结果会很奇怪。我的回答显示了如何将其替换为flatMap,但您需要这一步才能使其正常工作。
  • 谢谢,但是 permutations(str.substr(0, i) + str.substr(i + 1)) 如何返回 ['bc','cb']。它仍然对我没有意义。
  • 当我们查看 'b' 时使用 'bc' 大小写,那么所有其他字符都只是 ['c']。然后我们在'b'前面加上'bc'。同样,当我们查看 'c' 时,所有其他字符都只是 ['b']。然后我们在'c'前面加上'cb'。因此,整体输出为 ['bc','cb']
  • 正如@ScottSauyet 所说,您展示的内容是不可能的。你假设一个字符串的排列是一个字符串列表作为归纳假设,然后显示一个扩展步骤,它返回...... lists 字符串列表。所以类型不适合——列表必须被flatMap或reduce展平:)。 (一个有趣的旁注是这种并行的 b/w monadic join 和 fold/reduce,将列表的值(此处为列表)求和为一个值(列表)......(想知道“monad”是否可以说是“更高-以某种方式订购幺半群”......可能是的))
  • @CKate:您可以查看我的答案以了解更多详细信息。但基本的一点是,您可以假设该函数适用于更简单的情况,并表明它适用于当前情况。连同它适用于最简单情况的演示以及每个递归调用都是针对更简单情况的演示,您刚刚证明了它适用于所有情况。所以我们可以简单地假设 permutations('bc') 产生['bc', 'cb']。您始终可以选择以相同的方式浏览该示例。
【解决方案3】:

这实际上是permutations 的一个非常不寻常的定义,我碰巧从未见过。

(好吧,事实证明,我实际上写过一次an answer,它几乎完全等同,而且仅在几年前......哦,我的)。

伪代码中,通常看到的简单-递归定义是

perms [x, ...xs] = [ [...as, x, ...bs] | p <- perms xs, (as, bs) <- splits p]

但是这个是

perms2 xs = [ [x, ...p] | (as, [x, ...bs]) <- splits xs, p <- perms2 [...as, ...bs]]

(带有列表理解和模式;没有空列表案例;使用splits“自然” 定义,它构建了一个列表,其中包含将列表分成两部分的所有可能性)。

这里有一定的二元性……很有趣。而且不是“简单地”递归的。 :)

或者,用一些更明显的方式来实现命名函数,

perms [x, ...rest] = [ i | p <- perms rest, i <- inserts x p]
                   = flatMap (inserts x) (perms rest)

--- and this version, 
perms2 xs = [ [x, ...p] | (x, rest) <- picks xs, p <- perms2 rest]

另见:

【讨论】:

    猜你喜欢
    • 2021-01-27
    • 2012-08-31
    • 1970-01-01
    • 1970-01-01
    • 2013-03-20
    • 2021-11-16
    • 2019-11-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多