递归和尾递归

程序调用自身,称为递归。

递归是一个非常重要的算法思想,生活中也常见类似场景,比如排队时想知道前面还有几个人,需要向前问。再比如考试时学生向后传试卷,直到最后一个就将剩余的试卷还给老师。

什么样的情况下可以用递归?

(1)一个问题可以分解成多个子问题

(2)这个问题与分解成的子问题求解思路一致

(3)一定有一个终止条件

实现递归最核心的就是找到公式和终止条件。

以斐波那契数列举例:1 1 2 3 5 8 13

找公式,一个位置上的数字等于前两位数字之和,即f(n)=f(n-1)+f(n-2),终止条件:n<=2时f(n)=1

公式变成代码就很简单了:

public static int fab(int i) {
    if (i <= 2) {
        return 1;
    }
    return fab(i - 1) + fab(i - 2);
}    

输出前五个:1 1 2 3 5

但是这个递归的时间复杂度和空间复杂度非常高,为O(2^n),当尝试计算第40个数字就已经到了秒级。写段代码测试一下:

for (int i = 1; i < 50; i++) {
    long a = System.currentTimeMillis();
    fab(i);
    long b = System.currentTimeMillis();
    System.out.println("第" + i + "次耗时:" + (b - a));
}
第40次耗时:549
第41次耗时:1003
第42次耗时:1110
第43次耗时:1129
第44次耗时:1747
第45次耗时:2815
第46次耗时:4651

这种性能是我们不能容忍的,需要进行优化。最直接的是不使用递归,一般来说,递归都是可以使用别的办法解决的。比如这个地方,我们就用循环解决。

public static int loop(int n) {
  if (n <= 2){
     return 1;
  }
  int a = 1;   int b = 1;   int res = 0;   for (int i = 3; i <= n; i++) {     res = a + b;     a = b;     b = res;   }   return res; } 测试结果: 第46次耗时:0 第47次耗时:0 第48次耗时:0 第49次耗时:0

这样我们就优化到O(n)的复杂度。但是用循环又显得比较难看,我们追求更简洁的代码,还是递归更优雅,那之前的问题是同一个位置的数据计算过多,我们可以考虑加一层缓存,每次计算好了数据我们缓存起来,下一次可以直接取用。

private static int data[];
public static int cacheFab(int n) {
  if (n <= 2){
    return 1;
  }
  if (data[n] > 0) {
    return data[n];
  }
  int res = cacheFab(n - 1) + cacheFab(n - 2);
  data[n] = res;
  return res;
}

public static void main(String[] args) {
  int n = 50;
  data = new int[n];
  for (int i = 1; i < n; i++) {
    long a = System.currentTimeMillis();
    cacheFab(i);
    long b = System.currentTimeMillis();
    System.out.println("第" + i + "次耗时:" + (b - a));
  }
}
第46次耗时:0
第47次耗时:0
第48次耗时:0
第49次耗时:0

从上面可以看到,性能依然很高。但是还是使用了数组缓存,还可以进一步优化,就是使用尾递归。尾递归就是调用函数出现在末尾,这时候就不会创建新的栈,而且覆盖到前面去。

    /**
     * @param pre 上上次结果
     * @param res 上次结果
     * @param n
     * @return
     */
    public static int tailFab(int pre, int res, int n) {
        if (n <= 2) {
            return res;
        }
        return tailFab(res, pre + res, n - 1);
    }
    public static void main(String[] args) {
        for (int i = 1; i < 50; i++) {
            long a = System.currentTimeMillis();
            tailFab(1, 1, i);
            long b = System.currentTimeMillis();
            System.out.println("第" + i + "次耗时:" + (b - a));
        }
    }

第46次耗时:0
第47次耗时:0
第48次耗时:0
第49次耗时:0

这个性能也是O(n)的,代码简洁性能高,推荐用这种方式,如果工作中有递归的场景,可以尝试使用。

 

posted @ 2020-12-12 23:57  以战止殇  阅读(62)  评论(0编辑  收藏  举报