• 递归和尾递归


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

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

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

    (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)的,代码简洁性能高,推荐用这种方式,如果工作中有递归的场景,可以尝试使用。

  • 相关阅读:
    IT教育课程考评系统开发-07
    2020091201-1
    ip
    输入框枚举
    语言枚举
    《岁月神偷》弹唱和弦吉他谱_六线谱
    string 转化成 string数组
    获取类的字段值
    获取类的字段
    最全的省份递归
  • 原文地址:https://www.cnblogs.com/dlcode/p/14127412.html
Copyright © 2020-2023  润新知