Kotlin编程语言(十)——递归和尾递归

Kotlin编程语言(十)——递归和尾递归

系列文章目录

Kotlin编程语言(一)——Java到kotlin的演变过程
Kotlin编程语言(二)——Kotlin详解
Kotlin编程语言(三)——Kotlin和Java共同点
Kotlin编程语言(四)——Kotlin和Java差异点
Kotlin编程语言(五)——基础语法定义
Kotlin编程语言(六)——不同数量的引号定义字符串
Kotlin编程语言(七)——区间(ranges)
Kotlin编程语言(八)——Array、List和Map
Kotlin编程语言(九)——不同字符类型相互转换
Kotlin编程语言(十)——递归和尾递归



前言

在 Kotlin 中,递归和尾递归是函数式编程的重要概念。下面将分别介绍这两个概念以及它们在 Kotlin 中的使用方式。


一、递归(Recursion)

递归是指在一个函数或子程序中直接或间接调用自身的一种方法。每次递归调用都会创建一个新的栈帧,这可能会导致栈溢出问题。在编程中,递归可以用来解决那些可以通过更小规模的相同问题来表达的问题。

示例:计算累加

fun sum(n: Int): Int {
    return if (n == 1) 1 else n + sum(n - 1)
}

fun main() {
    val result = sum(10)
    println("The sum from 1 to 10 is: $result") // 输出: The sum from 1 to 10 is: 55
}

解释:

  • n不等于1时,函数会返回n + sum(n - 1)
  • 每次递归调用都会创建一个新的栈帧来保存当前的n值和部分计算结果。
  • 最终,当n==1时,递归停止,所有栈帧开始回溯并计算最终结果。

栈帧的创建和销毁

  1. 初始调用sum(10)

    • 创建一个栈帧,保存 n = 10
    • 计算 10 + sum(9),但 sum(9) 还未计算完成,所以不能立即返回结果。
  2. 递归调用sum(9)

    • 创建一个新的栈帧,保存 n = 9
    • 计算 9 + sum(8),但 sum(8) 还未计算完成,所以不能立即返回结果。
  3. 继续递归调用sum(8), sum(7), …, sum(1)

    • 每次递归调用都会创建一个新的栈帧,保存当前的 n 值。
    • 每次调用都会计算 n + sum(n - 1),但 sum(n - 1) 还未计算完成,所以不能立即返回结果。
  4. 递归终止sum(1)

    • 创建一个新的栈帧,保存 n = 1
    • 返回 1
  5. 回溯计算

    • 控制权返回到 sum(2),计算 2 + 1 = 3,返回 3
    • 控制权返回到 sum(3),计算 3 + 3 = 6,返回 6
    • 继续回溯,直到 sum(10),计算 10 + 45 = 55,返回 55

二、尾递归(Tail Recursion)

尾递归是一种特殊的递归形式,其中函数的最后一步操作是调用自身。由于尾递归函数的最后一个操作是递归调用,所以编译器可以优化这个过程,使其不会增加额外的栈帧,从而避免了因为过多的递归调用而导致的栈溢出错误

Kotlin 编译器支持尾递归优化,但是必须显式地使用 tailrec 关键字来标记一个函数是尾递归的。如果一个被标记为 tailrec 的函数不是尾递归的形式,编译器将会报错。

示例:尾递归计算累加:

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

fun main() {
    val result = sum(10)
    println("The sum from 1 to 10 is: $result") // 输出: The sum from 1 to 10 is: 55
}

解释:

  • n≠0 时,函数会返回sum(n - 1, n + accumulator)
  • accumulator参数用于累积中间结果。
  • 每次递归调用都是函数的最后一步操作,编译器可以优化这种调用,使其重用当前的栈帧。
  • 最终,当n==0时,递归停止,返回累积的结果accumulator

栈帧的创建和销毁

  1. 初始调用sum(10, 0)

    • 创建一个栈帧,保存 n = 10accumulator = 0
    • 计算 sum(9, 10 + 0),即 sum(9, 10)
  2. 递归调用sum(9, 10)

    • 更新当前栈帧中的 naccumulator,而不是创建新的栈帧。
    • 计算 sum(8, 19),即 sum(8, 19)
  3. 继续递归调用sum(8, 19), sum(7, 27), …, sum(0, 55)

    • 每次递归调用都会更新当前栈帧中的 naccumulator
    • 没有创建新的栈帧,只是修改现有的栈帧。
  4. 递归终止sum(0, 55)

    • n == 0 时,返回 accumulator,即 55

三、区别

递归

  • 每次递归调用都会创建一个新的栈帧。
  • 所有的栈帧在递归调用完成后才会逐个销毁。
  • 如果递归深度很大,可能会导致栈溢出。

尾递归

  • 编译器优化后,每次递归调用不会创建新的栈帧,而是重用当前的栈帧。
  • 只有一个栈帧被反复更新,直到递归终止。
  • 避免了栈溢出的风险,更高效。

四、图解

普通递归的栈帧图

sum(10) -> sum(9) -> sum(8) -> ... -> sum(1)
  |         |         |                |
  v         v         v                v
  10        9         8                1

尾递归的栈帧图

sum(10, 0) -> sum(9, 10) -> sum(8, 19) -> ... -> sum(0, 55)
  |           |            |                   |
  v           v            v                   v
  (10, 0)    (9, 10)     (8, 19)             (0, 55)

总结

  • 性能考虑:虽然尾递归可以提高效率并防止栈溢出,但它并不总是最佳选择。有时候,迭代算法可能更加直观且易于理解。
  • 编译器优化:只有当递归调用是函数体中的最后一个操作,并且没有其他后续的操作需要执行时,Kotlin 编译器才能对递归进行优化。
  • 内存消耗:非尾递归可能会导致大量的内存消耗,特别是对于深度较大的递归调用,因为每个调用都需要保存一个栈帧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值