你们都知道递归,尾递归呢?什么又是尾递归优化?

你们都知道递归,尾递归呢?什么又是尾递归优化?

码农唐磊 程序猿石头
你们都知道递归,尾递归呢?什么又是尾递归优化?
今天,咱们来聊聊递归函数。为啥忽然想到递归?其实就从电影名字《恐怖游轮》《盗梦空间》想到了。图片java

递归是啥?

递归函数你们确定写过,学校上课的时候,估计最开始的例子就是斐波拉契数列了吧。例如:面试

int Fibonacci(n) {
    if (n < 2) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

递归函数简而言之就是在一个函数中,又“递归”调用本身。在写递归函数的时候,须要注意的地方就是递归函数的结束条件。用递归函数确实能简化不少算法的实现,好比常见的二叉树遍历等。但每每在写递归函数的时候,最容易出现的问题就是所谓的“栈溢出”。算法

为何会有“栈溢出”呢?由于函数调用的过程,都要借助“栈”这种存储结构来保存运行时的一些状态,好比函数调用过程当中的变量拷贝,函数调用的地址等等。而“栈”每每存储空间是有限的,当超过其存储空间后,就会抛出著名的异常/错误“StackOverflowError”。ide

咱们以一个简单的加法为例,例如:函数

int sum(int n) {
    if (n <= 1) return n;
    return n + sum(n-1);
}

std::cout << sum(100) << std::endl;
std::cout << sum(1000000) << std::endl;

很简答,编译运行后,比较小的数字,能获得正确的答案,当数字扩大后,就会直接发生“segmentation fault”。性能

尾递归又是啥?

我得知这个概念,最开始仍是由于不少年前一次面试,面试官问我“你知道什么是尾递归吗?”,我觉得是“伪”递归,难道是假的递归???当初我也是懵逼状态(当初面试官忍住没笑也是厉害了)。从“尾”字可看出来即若函数在尾巴的地方递归调用本身。上面的例子写成尾递归,就变成了以下:优化

int tailsum(int n, int sum) {
    if (n == 0) return sum;
    return tailsum(n-1, sum+n);
}

能够试试结果,计算从 1 加到 1000000,仍然是segmentation fault。为何呢?由于这种写法,本质上仍是有多层的函数嵌套调用,中间仍然有压栈、出栈等占用了存储空间(只不过能比前面的方法会省部分空间)。scala

尾递归优化

当你给编译选项开了优化以后,见证奇迹的时刻到了,竟然能算出正确结果。如图所示:
你们都知道递归,尾递归呢?什么又是尾递归优化?code

C++ 默认 segmentation fault, 开启编译优化后,能正常计算结果。blog

缘由就是由于编译器帮助作了尾递归优化,能够打开汇编代码看看(这里就不展现 C++的了)。后面我用你们比较熟悉的 JVM based 语言 Scala 来阐述这个优化过程。(好像 Java 的编译器没作这方面的优化,至少我实验我本地 JDK8 是没有的,不清楚最新版本的有木有)(scala 自己提供了一个注解帮助编译器强制校验是否可以进行尾递归优化@tailrec)

object TailRecObject {

   def tailSum(n: Int, sum: Int): Int = {
        if (n == 0) return sum;
        return tailSum(n-1, n+sum);
   }

   def main(args: Array[String]) {
      println(tailSum(100, 0))
      println(tailSum(1000000, 0))
   }

}

结果以下图所示,默认状况下 scalac 作了尾递归优化,可以正确计算出结果,当经过 -g:notailcalls 编译参数去掉尾递归优化后,就发生了 Exception in thread "main" java.lang.StackOverflowError了。
你们都知道递归,尾递归呢?什么又是尾递归优化?

默认启用尾递归优化正常计算结果,禁用尾递归优化则“StackOverflow”。
咱们来看看生成的字节码有什么不一样。
你们都知道递归,尾递归呢?什么又是尾递归优化?

包含尾递归优化的字节码,直接 goto 循环。
你们都知道递归,尾递归呢?什么又是尾递归优化?

禁用尾递归优化的字节码,方法调用。

从上面能够看出,尾递归优化后,变成循环了(前面的 C++ 相似)。

好了,尾递归我们就了解到这里。我的见解,咱们知道有“尾递归”这个点就行了,有时候咱们写递归就是为了方便,代码可读性好,若是确实是出于性能考虑,咱们能够本身用迭代的方式去实现,不依赖于具体的编译器实现。固然对于像 scala 这样,有一些语法糖可以帮助校验和验证,也是一个不错的选择。但递归转迭代的能力,咱们能具有岂不更好。

下次想聊什么话题吗?欢迎留言。老规矩,若是有帮助(对你身边的其余人有帮助也行呀,一点帮助也没有的话应该也不会看到这里了吧),写篇文章真心不易,但愿亲多多帮忙“在看”,转发分享支持。

相关文章
相关标签/搜索