【问题标题】:Tail recursion in mutually recursive functions相互递归函数中的尾递归
【发布时间】:2019-01-19 01:47:59
【问题描述】:

我有以下代码:

let rec f n =
    if n < 10 then "f" + g (n+1) else "f"
and g n =
    if n < 10 then "g" + f (n+1) else "g"

我想让这些相互递归的函数尾递归进行优化。 我尝试了以下方法:

let rec fT n = 
    let rec loop a = 
        if n < 10 then "f" + gT (a) else "f"
    loop (n + 1) 
and gT n =
    let rec loop a = 
        if n < 10 then "g" + fT (a) else "g"
    loop (n + 1) 

这是一个正确的尾递归版本吗?如果没有,将不胜感激在正确方向上的提示。

编辑(第二次解决):

let rec fA n = 
    let rec loop n a = 
        if n < 10 then loop (n + 1) ("f" + a) else a
    loop n "f"
and gA n =
    let rec loop n a = 
        if n < 10 then loop (n + 1) ("g" + a) else a
    loop n "g"

编辑(第三次提出解决方案):

let rec fA n a = 
    if n < 10 then gA (n + 1) (a + "f") else a
and gA n a =
    if n < 10 then fA (n + 1) (a + "g") else a

编辑(正确的解决方案):

let rec fA n a = 
    if n < 10 then gA (n + 1) (a + "f") else (a + "f")
and gA n a =
    if n < 10 then fA (n + 1) (a + "g") else (a + "g")

【问题讨论】:

  • 这就是“请帮我完成作业”的问题:寻求提示而不是解决方案,以便您自己解决。 干得好。 我要补充的唯一一条建议是:下次,请提前提及这是一项家庭作业,因此您不希望人们只给您答案。 Stack Overflow 上的一些人可能很有帮助,所以如果你提前说“这是作业,我需要自己解决,我只需要一个小提示”,那么人们可以校准他们的回答正确的帮助级别。

标签: recursion f# f#-interactive


【解决方案1】:

您的解决方案绝对不是尾递归的。

“尾递归”是这样的递归,其中每个递归调用都是函数所做的最后件事。这个概念很重要,因为它意味着运行时可以选择在调用之间不保留堆栈帧:因为递归调用是最后一件事,调用函数在此之后不需要做任何其他事情,运行时可以跳过将控制权返回给调用函数,并将被调用函数返回给顶级调用者。这允许表达任意深度的递归算法,而不必担心堆栈空间不足。

然而,在您的实现中,函数fT.loop 调用函数gT,然后将“f”添加到gT 返回的任何内容之前。 “f”的前缀发生在gT 返回之后,因此对gT 的调用不是fT.loop 所做的最后一件事。因此,它不是尾递归的。

为了将“常规”递归转换为“尾”类,可以这么说,您必须“将逻辑从里到外”。让我们看一下函数f:它调用g,然后在g 返回的任何东西前面加上“f”。这个“f”前缀是函数f在总计算中的全部“贡献”。现在,如果我们想要尾递归,这意味着我们不能在递归调用之后做出“贡献”。这意味着贡献必须发生在之前。但是如果我们在调用之前做了贡献,之后什么都不做,那么我们如何避免失去那个贡献呢?唯一的方法是将贡献作为参数传递给递归调用

这是尾递归计算背后的一般思想:我们不是等待嵌套调用完成然后向输出添加一些内容,而是先进行添加,然后将“到目前为止”添加的内容传递给递归调用.

回到您的具体示例:由于f 的贡献是“f”字符,它需要将此字符添加到“到目前为止”计算的内容中,并将其传递给递归调用,然后做同样的事情,依此类推。 “到目前为止”参数的语义应该是“计算你要计算的任何东西,然后在前面加上我的‘到目前为止’”。

由于您只要求“提示”,这显然是作业(如果我错了,请原谅我),我不会发布实际代码。如果您愿意,请告诉我。

【讨论】:

  • 感谢您的详细解答。我已经用我的新解决方案更新了我上面的问题,尽管它似乎没有输出正确的值。我想在 F 的递归循环中从 G 调用内部递归循环,就像我在上一个“解决方案”中调用 gA 中的 fA 一样,反之亦然,尽管这给了我一个错误。我在正确的轨道上吗?
  • 你快到了!除非您还没有完全完全将计算“翻过来”。原始计算预先将字符添加到嵌套调用的结果中。因此,转换后的计算需要将它们追加到外部调用的结果中。计算累积以相反的顺序发生。此外,失去内部功能:它们不会添加任何东西。
  • 谢谢,这很有道理。我在上面添加了第三个解决方案。
  • 我看到了您的第三个解决方案,但它仍然不是相当。考虑极端情况:当您调用f 10 时会发生什么?当您拨打fA 10 "" 时会发生同样的事情吗?为什么?
  • 哦,我明白了。我只需要在 fA 的 else 语句中添加 (a + "f") 并在 gA 中添加 (a + "g")' 对吗?否则,fA 10 将返回一个空字符串,因为累加器从一开始就是空的。 ?
【解决方案2】:

我赞同您的尝试绝对不会将递归置于尾部位置的观察

我将如何处理将递归移动到尾部位置将使用延续。为此,我们必须实现具有延续参数的fkgk 变体,然后可以分别使用fkgk 实现fg

我不是 F# 专家,但我可以用 JavaScript 简单地说明这一点。我通常不会使用其他语言发布答案,但由于语法非常相似,我认为它会对您有所帮助。它还具有额外的好处,您可以在浏览器中运行此答案以查看它的工作情况

// f helper
let fk = (n, k) => {
  if (n < 10)
    return gk(n + 1, g => k("f" + g))
  else
    return k("f")
}
    
// g helper
let gk = (n, k) => {
  if (n < 10)
    return fk(n + 1, f => k("g" + f))
  else
    return k("g")
}
    
let f = n =>
  fk(n, x => x)

let g = n =>
  gk(n, x => x)
  
console.log(f(0))  // fgfgfgfgfgf
console.log(g(0))  // gfgfgfgfgfg
console.log(f(5))  // fgfgfg
console.log(g(5))  // gfgfgf
console.log(f(11)) // f
console.log(g(11)) // g

【讨论】:

  • 继续传递风格与“尾递归”不太一样。这是下一步,在这种情况下绝对是矫枉过正。
  • @FyodorSoikin 同意它们不一样——延续传递风格是一种(多种)技术,可用于将非尾递归过程转换为尾递归过程。 fkgk 在这种情况下是尾递归的,但我看不出它们是多么的矫枉过正。诚然,我不知道处理这个问题的 F# 习语,所以也许你可以在你的答案中分享一些代码示例^_^
  • 这有点过头了,因为它不是直接产生输出,而是创建一个闭包树,然后“反向”执行以产生输出。额外的步骤、额外的时间、额外的记忆。
猜你喜欢
  • 2016-06-13
  • 2019-09-14
  • 1970-01-01
  • 2016-01-06
  • 2020-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-10
相关资源
最近更新 更多