当前位置: 首页 > 知识库问答 >
问题:

如何让这个函数是尾递归的?

严柏
2023-03-14

我仍在尝试实现2-3个手指树,并取得了良好的进展(存储库)。在做一些基准测试时,我发现当树非常大时,我非常基本的toList会导致堆栈溢出异常。起初,我看到了一个简单的修复方法,并将其设置为尾部递归。

不幸的是,事实证明,toList不是罪魁祸首,但viewr是:

/// Return both the right-most element and the remaining tree (lazily).
let rec viewr<'a> : FingerTree<'a> -> View<'a> = function
    | Empty -> Nil
    | Single x -> View(x, lazyval Empty)
    | Deep(prefix, deeper, One x) ->
        let rest = lazy (
            match viewr deeper.Value with
            | Nil ->
                prefix |> Digit.promote
            | View (node, lazyRest) ->
                let suffix = node |> Node.toList |> Digit.ofList
                Deep(prefix, lazyRest, suffix)
        )
        View(x, rest)
    | Deep(prefix, deeper, Digit.SplitLast(shorter, x)) ->
        View(x, lazy Deep(prefix, deeper, shorter))
    | _ -> failwith Messages.patternMatchImpossible

寻找唯一的递归调用很明显,这不是尾部递归。不知何故,我希望这个问题不会存在,因为这个调用被包装在一个类似于连续的延迟中。

我听说并读过连续体,但到目前为止,我从未使用过连续体。我想我真的需要。我已经盯着代码看了很长一段时间,到处放函数参数,在其他地方调用它们……我完全迷路了!

如何做到这一点?

更新:调用代码如下所示:

/// Convert a tree to a list (left to right).
let toList tree =
    let rec toList acc tree =
        match viewr tree with
        | Nil -> acc
        | View(head, Lazy tail) -> tail |> toList (head::acc)
    toList [] tree

更新2:导致崩溃的代码是这个。

let tree = seq {1..200000} |> ConcatDeque.ofSeq
let back = tree |> ConcatDeque.toList

我检查了一下,树构建得很好,只有12层深。是第2行中的调用触发了溢出。

更新3:kvb是对的,我之前遇到的管道问题与此有关。重新测试调试/发布和有/无管道的交叉乘积,它在所有情况下都有效,只有一种情况除外:管道操作员的调试模式崩溃。32位与64位的行为相同。

我很确定我在发布问题时运行的是发布模式,但今天它起作用了。也许还有其他因素…抱歉。

虽然崩溃已经解决了,但出于理论兴趣,我将这个问题留给大家。毕竟,我们是来学习的,不是吗?

因此,让我修改这个问题:
从代码来看,viewr肯定不是尾部递归的。为什么它不总是爆炸,如何用连续体重写它?


共有1个答案

楚彦
2023-03-14

调用viewr从来不会导致立即递归调用viewr(递归调用受lazy保护,并且在调用viewr的其余部分中不会强制),因此不需要使其尾部递归以防止堆栈无约束增长。也就是说,对viewr的调用创建了一个新的堆栈帧,当viewr的工作完成时,该堆栈帧立即弹出;然后,调用方可以强制延迟值,为嵌套的viewr调用生成新的堆栈帧,然后立即再次弹出,等等,因此重复此过程不会导致堆栈溢出。

 类似资料:
  • 我有一个家庭作业问题,它给出了一个递归函数,我必须使用尾部递归来实现它。函数为f(0)=1 f(n)=1 2*f(n-1) 我并不擅长尾部递归,我试着查找示例,但我发现的都是没有斐波那契序列的示例,这没有多大帮助。 我真正拥有的是 我知道尾递归基本上每次调用都计算函数,我只是不知道如何实现它。 编辑:我打了一个错字f(n)应该是1 2*f(n-1)

  • 我试图编写一个函数repeat(s:String,n:Int),它将串接字符串s n次并返回它,但由于某种原因,我没有得到正确的结果,并且得到了一个错误,即它不是尾部递归的,我在逻辑上很难理解为什么它不是尾部递归的。 在连接完成之前,是否必须处理递归?我该如何解决这个问题?使递归重复(s,n-1)不起作用,因为它会递归s太多次,但我不确定还有什么其他方法可以做到。 ps:我这么做主要是为了练习递归

  • 假设我编写这样的代码: 我如何让Kotlin优化这些相互递归的函数,以便在不抛出StackOverflower错误的情况下运行main?tailrec关键字适用于单函数递归,但没有更复杂的功能。我还看到一个警告,在使用关键字tailrec的地方没有找到尾部调用。也许这对编译器来说太难了?

  • 各种各样的书籍、文章、博客帖子表明,将递归函数重写为尾部递归函数可以加快速度。毫无疑问,对于生成斐波那契数或计算阶乘等琐碎情况,它会更快。在这种情况下,有一种典型的重写方法,即使用“辅助函数”和用于中间结果的附加参数。 尾部递归很好地描述了尾部递归函数和非尾部递归函数之间的差异,以及如何将递归函数转换为尾部递归函数。对于这种重写来说什么是重要的-函数调用的数量是相同的(重写之前/之后),不同之处在

  • 我想我理解了教科书中对尾部递归函数的定义:在函数调用后不执行任何计算的函数。我还发现,作为一个结果,尾部递归函数的内存效率会更高,因为它每次调用只需要一条记录,而不是每次都需要保留一条记录(就像在普通递归中那样)。 我不太清楚的是,这个定义如何应用于嵌套调用。我将提供一个例子: 我最初给出的答案是,根据定义,它不是尾部递归的(因为外部调用是在计算内部调用之后执行的,所以其他计算是在第一次调用之后完