尾递归的那点事
学习就是不断解惑的过程:
先找个问题:前几天偶然有人问到一个关于栈溢出的问题,说递归时为什么scala不容易比python出现栈溢出的问题?大概是这个意思,原话记不住了,当时也没多想,只是联想到递归是从上到下的计算流程,所以估计可能是记忆递归不必保存那么多栈帧或者是从下到上的计算流程,今天突然想到这个问题,记录一下。
回忆只需要看这里
- 一句话结论:
scala编译器会将<尾递归>优化为<累加器+迭代器>的循环计算
- @tailrec:用来验证这个方法是否会使用尾递归优化进行编译,如果不能会报
Recursive call not in tail position
笔记
问题有了,需要将问题拆分,然后一个个寻找答案
- 栈溢出是什么意思?
- 什么是递归?
- 什么是尾递归?
- 递归的优缺点分别是什么?
- 递归如何优化?
1.栈溢出是什么意思
栈溢出
指的就是程序中定义的引用、指针等超过了栈结构预先设定的内存大小,最常见的就是递归调用,常见的报错如下:
Exception in thread "main" java.lang.StackOverflowError
2.什么是递归
递归
: 直接或间接的调用自己,通过参数迭代,减小原问题的规模/量级,进而实现[大问题->小问题]的转换思想
[Recursion in Wiki]In computer science, recursion is a method of solving a problem where the solution depends on solutions to smaller instances of the same problem
3.什么是尾递归
尾递归
: 顾名思义,递归调用放在尾部的递归方式,如:
def bee(num: Int, acc: Long): Long = {
if (num <= 1) acc
else bee(num - 1, acc + num)
}
4.递归的优缺点
优点
- 书写优雅、简单
- 逻辑符合动态规划的思路,清晰明了
缺点
- 需要保留大量的栈帧,经常出现栈溢出
5.如何优化
背景案例:求解1+2+3+4+…+n的和
def foo(num: Int): Long = {
if (num <= 1) num.toLong
else num + foo(num - 1)
}
递归的思想可以理解为:[从上到下的思维]要求解1->n的和,只需要将1->n-1的和求出来,再加上n即可<称为相关关系
>,当n=1时,和可以很轻松计算为1<称为base case
>;然后就开始按照保存的调用关系<即1-4的步骤>,计算最终结果<即5->8的步骤>
1->
2 ->
3 ->
4 ->
5 <-
6 <-
7 <-
8<-
很明显,上面保存的调用关系[1-4]是可以优化掉的,只需要我们每次将上次的计算结果保存并传递给后续计算[累加器],然后通过计数等方式界定计算次数[迭代器/计数器]
def bar(num: Int): Long = {
var acc = 0L;
for (i <- 1 to num) { acc += i }
acc
}
为了保持递归的优雅,将上述写法修改成尾递归的形式,尾递归中不得出现递归调用赋值、计算等操作。
@tailrec
def bee(num: Int, acc: Long): Long = {
if (num <= 1) acc
else bee(num - 1, acc + num)
}