尾递归学习笔记-MY

144 阅读3分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

按照习惯先上代码,然后讲解问题。

疑问

  1. 递归与迭代的区别?

  2. 递归有什么优点?

  3. 为什么递归那么好用,还需要尾递归?

  4. 递归在Java中的使用问题?

代码

迭代式的阶乘计算

static long factorialIterative(long n){
    long r = 1;
    for (int i = 1; i <=n ; i++) {
        r *= i;
    }
    return r;
}

递归式的阶乘计算

static long factorialRecursive(long n){
    return n == 1 ? 1 : n * factorialRecursive(n-1);
}	

基于Stream的阶乘

static long factorialStreams(long n){
    return LongStream.rangeClosed(1,n)
        .reduce(1,(long a, long b) -> a * b);
}

基于尾递归的阶乘

static long factorialTailRecursive(long n){
    return factorialHelper(1, n);
}
static long factorialHelper(long acc, long n){
    return n == 1 ? acc : factorialHelper(acc * n, n-1);
}

问题解答

递归与迭代的区别?

迭代是什么?在Java中,迭代可以使用Iterator。并且java中的增强for循环底层使用的就是迭代器。可以想象一下,我们有个指针,一直在内存中找数据,找到一个返回数据并移动到下一个数据上。

递归是什么?我们可以这样理解,递归就是将一个大的问题,分解到小问题去解决(有点类似动态规划),并且在解决问题的过程中调用函数自身,此函数的功能并没有改变,一直解决相同的问题。

递归有递归条件和基线条件,递归条件就是什么时候可以开启递归,基线条件是什么时候可以终止递归。

ArrayList<Object> list = new ArrayList<>();
list.add("h");
list.add("e");
list.add("l");
list.add("l");
list.add("o");
Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}
// 增强for循环
for (Object o : list) {
    System.out.println(o);
}

递归有什么优点?

装ABC[1]。

使代码阅读起来轻松愉悦,因为递归写的代码十分优雅简洁。有人说,递归符合思维习惯,我感觉哪个代码写起来都符合思维习惯😀。

为什么递归那么好用,还需要尾递归?

在我们思考问题的时候,去思考为什么是个好习惯。人们使用一个新鲜的事物并坚持长期使用,一定是此事物的某个功能满足了他,甚至的这个功能的有点盖过了缺点。

递归在Java这些语言上存在一个严重问题,如果递归的深度过大,就会出现栈溢出!每一次的递归都会创建的一个栈帧,每个栈帧保存了了一个函数的状态,对于递归而言就是递归函数本身,并且此函数没有运行完,栈帧中保存了该函数的返回地址和局部变量。

尾递归是在返回值上进行计算,避免了栈帧的无限创建。简而言之,尾递归避免了递归带来的栈溢出。但是!在Java中,没有对尾递归进行优化,一样会出现尾递归的栈溢出。😂

递归在Java中的使用问题?

上面说了,递归在Java中存在栈溢出问题,尾递归也可能出现栈溢出问题。所以需要使用递归的时候,可以使用Stream流来避免。

总结

return n == 1 ? 1 : n * factorialRecursive(n-1);

来分析下上面的递归我们可以写成if-else的形式,方便直观的看。

if (n==1){
    return 1;
}else{
    return n * factorialRecursive(n-1);
}

这里的递归在返回值这里,直接进行函数状态保存的。

下面看看尾递归

return n == 1 ? acc : factorialHelper(acc * n, n-1);
if (n==1){
    return 1;
}else{
    return factorialHelper(acc * n, n-1);
}

在尾递归中,数值的计算放在了尾部,实际上是已经计算完成的(acc * n),所以不会无限的创建栈帧,避免了栈溢出。

这里说下LongStream.rangeClosed()与LongStream.range()区别,前者是闭区间[a,b],后者是开区间(a,b)。

举个例子,LongStream.rangeClosed(1,5)是1,2,3,4,5,LongStream.range(1,5)是1,2,3,4。