ZetCode

Kotlin tailrec 关键字

最后修改于 2025 年 4 月 19 日

Kotlin 的 tailrec 关键字为递归函数启用尾递归优化。这种优化通过将递归转换为迭代来防止堆栈溢出错误。本教程将通过实际示例深入探讨 tailrec 关键字。

基本定义

尾递归是递归的一种特殊形式,其中递归调用是函数中的最后一个操作。tailrec 修饰符告诉 Kotlin 编译器将此递归优化为高效的循环。这避免了深度递归的堆栈溢出。

基本 tailrec 示例

尾递归的最简单示例是计算阶乘。然而,阶乘本身不是尾递归的。这是一个使用累加器的尾递归版本。

Factorial.kt
package com.zetcode

fun factorial(n: Int): Int {
    tailrec fun fact(n: Int, acc: Int): Int {
        return if (n == 0) acc else fact(n - 1, n * acc)
    }
    return fact(n, 1)
}

fun main() {
    println(factorial(5)) // Output: 120
}

此示例显示了一个嵌套的尾递归函数。对 fact 的递归调用是最后一个操作。编译器将其优化为使用常量堆栈空间。累加器保存中间结果。

数字求和

计算从 1 到 n 的数字之和是一个经典的尾递归问题。以下是如何使用 tailrec 实现它。

Sum.kt
package com.zetcode

fun sum(n: Int): Int {
    tailrec fun sumTail(n: Int, acc: Int): Int {
        return if (n == 0) acc else sumTail(n - 1, acc + n)
    }
    return sumTail(n, 0)
}

fun main() {
    println(sum(100)) // Output: 5050
}

sumTail 函数在 acc 参数中累加总和。每次递归调用将当前数字添加到累加器。当 n 达到 0 时,递归停止。

斐波那契数列

斐波那契数列可以通过尾递归有效地实现。这避免了朴素递归实现的指数时间复杂度。

Fibonacci.kt
package com.zetcode

fun fibonacci(n: Int): Int {
    tailrec fun fib(n: Int, a: Int, b: Int): Int {
        return if (n == 0) a else fib(n - 1, b, a + b)
    }
    return fib(n, 0, 1)
}

fun main() {
    println(fibonacci(10)) // Output: 55
}

此实现使用两个累加器(a 和 b)来保存前两个斐波那契数。递归调用移动这些值并计算下一个数字。这以 O(n) 时间和常量空间运行。

列表操作

尾递归非常适用于列表操作。以下是如何使用尾递归反转列表。

ReverseList.kt
package com.zetcode

fun <T> reverseList(list: List<T>): List<T> {
    tailrec fun reverseTail(remaining: List<T>, acc: List<T>): List<T> {
        return if (remaining.isEmpty()) acc 
               else reverseTail(remaining.drop(1), listOf(remaining.first()) + acc)
    }
    return reverseTail(list, emptyList())
}

fun main() {
    println(reverseList(listOf(1, 2, 3, 4, 5))) // Output: [5, 4, 3, 2, 1]
}

reverseTail 函数在累加器中构建反向列表。每次递归调用获取剩余列表的第一个元素,并将其添加到累加器。这有效地反转了列表。

GCD 计算

计算最大公约数 (GCD) 的欧几里得算法本质上是尾递归的。这是 Kotlin 的实现。

Gcd.kt
package com.zetcode

fun gcd(a: Int, b: Int): Int {
    tailrec fun gcdTail(a: Int, b: Int): Int {
        return if (b == 0) a else gcdTail(b, a % b)
    }
    return gcdTail(a, b)
}

fun main() {
    println(gcd(48, 18)) // Output: 6
}

gcdTail 函数实现了欧几里得算法。递归调用是最后一个操作,使其成为尾递归。编译器将其优化为使用常量堆栈空间。

幂计算

使用尾递归和平方求幂方法可以有效地计算幂。

Power.kt
package com.zetcode

fun power(base: Int, exponent: Int): Int {
    tailrec fun powerTail(base: Int, exponent: Int, acc: Int): Int {
        return when {
            exponent == 0 -> acc
            exponent % 2 == 1 -> powerTail(base, exponent - 1, acc * base)
            else -> powerTail(base * base, exponent / 2, acc)
        }
    }
    return powerTail(base, exponent, 1)
}

fun main() {
    println(power(2, 10)) // Output: 1024
}

此实现以 O(log n) 时间有效地计算幂。累加器保存中间结果。该函数以不同的方式处理奇数和偶数指数,以优化计算。

二分查找

二分查找可以通过尾递归实现,尽管它通常以迭代方式完成。这是尾递归版本。

BinarySearch.kt
package com.zetcode

fun binarySearch(list: List<Int>, target: Int): Int? {
    tailrec fun search(low: Int, high: Int): Int? {
        if (low > high) return null
        val mid = (low + high) / 2
        return when {
            list[mid] == target -> mid
            list[mid] < target -> search(mid + 1, high)
            else -> search(low, mid - 1)
        }
    }
    return search(0, list.size - 1)
}

fun main() {
    val list = listOf(1, 3, 5, 7, 9, 11, 13)
    println(binarySearch(list, 7)) // Output: 3
}

search 函数递归地实现了二分查找。递归调用是尾调用,因此编译器对其进行优化。这保持了二分查找的 O(log n) 时间复杂度。

tailrec 的最佳实践

来源

Kotlin 尾递归文档

本教程深入介绍了 Kotlin 的 tailrec 关键字,展示了如何优化递归函数。我们探讨了各种场景,包括数学运算、列表处理和搜索算法。正确使用尾递归可以使您的递归函数更有效、更安全。

作者

我的名字是 Jan Bodnar,我是一位热情的程序员,拥有多年的编程经验。自 2007 年以来,我一直在撰写编程文章。到目前为止,我已经撰写了 1400 多篇文章和 8 本电子书。我拥有超过八年的编程教学经验。

列出 所有 Kotlin 教程