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

Kotlin:相互递归函数的尾部递归

孔阎宝
2023-03-14

假设我编写这样的代码:

tailrec fun odd(n: Int): Boolean =
        if (n == 0) false
        else even(n - 1)

tailrec fun even(n: Int): Boolean =
        if (n == 0) true
        else odd(n - 1)

fun main(args:Array<String>) {
    // :( java.lang.StackOverflowError
    System.out.println(even(99999))
}

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

共有3个答案

裴姚石
2023-03-14

以下是@comonad蹦床建议的实现。它起作用了!

import kotlin.reflect.KFunction

typealias Result = Pair<KFunction<*>?, Any?>
typealias Func = KFunction<Result>

tailrec fun trampoline(f: Func, arg: Any?): Any? {
    val (f2,arg2) = f.call(arg)
    @Suppress("UNCHECKED_CAST")
    return if (f2 == null) arg2 
        else trampoline(f2 as Func, arg2)
}

fun odd(n: Int): Result =
        if (n == 0) null to false
        else ::even to n-1

fun even(n: Int): Result =
        if (n == 0) null to true
        else ::odd to n-1

fun main(args:Array<String>) {
    System.out.println(trampoline(::even, 9999999))
}
扈德容
2023-03-14

维基百科https://en.wikipedia.org/wiki/Tail_call:

尾部调用是作为过程的最终操作执行的子例程调用。如果尾部调用可能导致同一子例程稍后在调用链中再次调用,则该子例程称为尾部递归

所以你的案例根据定义不是尾递归。警告不是这么说的。

目前,编译器无法优化它,主要是因为这是一种非常罕见的情况。 但我不确定即使是Haskel也会优化它。

庄星汉
2023-03-14

您正在寻找的是“正确的尾调用”。JVM不支持这些,所以您需要蹦床。

正确的尾部调用会在跳转(而不是调用)到尾部调用的函数之前清除其自身函数(参数、局部变量)的内存。这样,尾部调用的函数可以直接返回到其调用者调用函数。无限相互递归是可能的。(在函数式语言中,这是最重要的功能之一。)

要在汇编程序中允许正确的尾调用,您需要一个命令跳转(goto)到通过指针引用的例程/方法。OOP需要调用(存储要跳回堆栈上的位置,然后跳转)到通过指针引用的例程/方法。

您可以使用蹦床设计模式模拟正确的尾调用,也许通过库有一些支持。蹦床是一个时循环,它调用一个函数,该函数返回对下一个函数的引用,该函数返回对下一个函数的引用...

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

  • 我正在研究这个简单的问题来练习基本Kotlin,在递归返回行上遇到了堆栈溢出,代码如下: 我对tailrec的理解是,它应该将递归函数转换为迭代函数,而迭代函数不会受到这种崩溃的影响。如果我没有正确实现尾部递归,编译器应该会发出错误。 有人能解释一下为什么这会像标准递归调用一样在大输入上崩溃吗?

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

  • 我有一个关于如何将“递归”转换为“尾部递归”的问题。 这不是家庭作业,只是在我试图润色算法书籍中的递归定理时出现的一个问题。 我熟悉使用递归的两个典型示例(阶乘和斐波那契序列),也知道如何以递归方式和尾部递归方式实现它们。 我的代码如下(我使用Perl只是为了使其简单,但可以轻松地转换为C/Java/C)。 运行代码时,输出如下: 递归函数在返回之前使用不同的参数调用自己两次。我尝试了几种方法将其

  • 我对函数式编程很陌生,尤其是下面使用的Scheme。我正在尝试使以下函数是递归的,尾递归的。基本上,该函数的作用是对两个字符串的对齐方式进行评分。当给定两个字符串作为输入时,它会比较每个“列”字符,并根据在称为 scorer 的函数中实现的评分方案(由下面的代码中的函数调用)来累积该对齐的分数。 我有一个想法,用一个帮助函数来累积分数,但我不太确定如何去做,因此我该如何让下面的函数尾递归呢?

  • 尾递归优化 recur 尾递归优化是函数式编程不能缺少的一个性能优化方案. 没有尾递归, 常有的递归调用也会形成很深的调用栈消耗内存. cljs 和 Clojure 类似, 都需要通过声明 recur 进行优化. 最终代码会被编译为 white 结构的 js 代码,从而提高性能. (defn factorial [acc n] (if (<= n 1) acc (recur (* ac