蓝桥杯 出栈顺序问题引发的思考以及递归的优化(缓存池)

蓝桥杯 出栈顺序问题引发的思考以及递归的优化(缓存池)

关于递归的优化和思考

在我们IT圈内有句话,普通程序员用迭代,天才程序员用递归。诚然,递归确实能够将许多复杂的问题简化,但是问题来了,由于递归采用自顶向下的运算方式,未结果优化的递归往往做了大量的重复运算,这就给人产生了递归无用,递归耗时这一印象,但是不要忘了“天才程序员用递归”,如果善用递归,非但不会出现耗时的情况,反倒让代码简介易懂,高效,下面笔者选用一道蓝桥杯的真题出栈问题来揭开递归的神秘面纱!


在网上能找到大量的算法题(蓝桥杯、ACM等),由于这些算法题晦涩难懂,
很多文章直接给出代码,造成读者理解上的困难。
同时在网上也很难找到算法优化类的文章,利用这段特殊的时间,
笔者觉得有必要自己写一篇通俗易懂的文章,以下选用了两个较为经典的例子

递归会做大量重复运算 笔者自己画了一个递归树示意图
在这里插入图片描述

**

在此先引用著名的斐波那契数列(递归)

逐步引入下文的优化过程,必要知识准备

先来看下低效的普通代码
(递归实现斐波那契数列)
//说明 以下所有代码基于Java实现 为方便读者区分代码,类名使用中文名(不推荐使用中文名,易导致未知问题)

import java.util.Scanner;
public class 斐波那契数列_普通递归 {
        public static void main(String[] args) {
            System.out.println("输入斐波那契数列的项数n:");
            Scanner sc = new Scanner(System.in);
            int n = sc.nextInt();
            long start = System.currentTimeMillis();    //记录递归开始时间      时间单位:毫秒
            System.out.println(fibonacci(n));
            long end = System.currentTimeMillis();      //记录递归结束时间
            System.out.println("本次递归项数为:"+n+",耗时"+(end-start)/1000+"s");
        }

        public static long fibonacci(long number) {
            if ((number == 0) || (number == 1))
                return number;
            else
                return fibonacci(number - 1) + fibonacci(number - 2);
        }
}

在这里插入图片描述
纳尼???求前50项数要49s,难不成太上老君炼丹也要七七四十九天???估计这段代码被老板看见当天就被炒鱿鱼了,递归的低效性被这以上代码体现的淋漓尽致,以上代码虽然简单,代价是太太太耗时了。


public class 斐波那契数列_递推 {
    public static double fib(int n){
        double f0=1;
        double f1=1;
        double i=2;
        double fn=0;
        for(i=2;i<n;i++){
            fn=f0+f1;
            f0=f1;
            f1=fn;
        }
        return fn;
    }
    public static void main(String[] args) {
        System.out.println("输入斐波那契数列的项数n:");
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        long start = System.currentTimeMillis();    //记录递归开始时间      时间单位:毫秒
        System.out.printf("%.0f",fib(n));
        long end = System.currentTimeMillis();      //记录递归结束时间
        System.out.println("本次递归项数为:"+n+",耗时"+(end-start)/1000+"s");
    }
}

在这里插入图片描述

附上个普通的高效代码(递推)和运行截图,不重点讨论,相信笔者都能看懂,为防止越界,我将返回值类型设为double,尽情虐代码吧!!!

下方高能,拿好小板凳,笔和笔记本

  • [它来了,它来了,它终于来了,重头戏来了 ] 大名鼎鼎的递归缓存池来也!!!
  • 嗯哼,哎呀,说白了递归缓存池就是一个数组啦,也没啥,但要注意喽,数组一定要设置为全局变量哦,不然起不了作用滴@_@
  • 先上代码↓↓↓
 import java.util.Scanner;
public class 斐波那契数列_缓存池 {
    //全局变量位于任何方法之外,设置缓存池长度为500  确保足够用 可根据世纪需要修改
    static double[] pool = new double[500];     //缓存池
    public static void main(String[] args) {
        System.out.println("输入斐波那契数列的项数n:");
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        long start = System.currentTimeMillis();    //记录递归开始时间      时间单位:毫秒
        System.out.printf("%.0f\n",fibonacci(n));
        long end = System.currentTimeMillis();      //记录递归结束时间
        System.out.println("本次递归项数为:"+n+",耗时"+(end-start)+"ms");    //此处改用ms作为单位  准确记录时间
    }

    public static double fibonacci(int number) {
        if ((number == 2) || (number == 1))
            return 1;
        if(pool[number]!=0)  //访问缓存池 判断缓存池中有没有记录当前运算的结果 有就直接返回结果
            return pool[number];
        //缓存池中找不到 将需要求解的运算值用一个变量存下来,再放到缓存池
        double x = fibonacci(number - 1) + fibonacci(number - 2);
        pool[number]=x;
        return x;
    }
}

先期待一波*_*
效果图来袭,准备好了吗??????
在这里插入图片描述

what happen???让我好好缓缓,说好的低效呢???神马???计算第50项用时6ms,刚才低效递归不是七七四十九秒吗???@_@,这运行效率提升了49s/7ms==??我拿计算器算算(疯狂按计算器中)。。。缓存池是个啥玩意???有那么神奇吗,是不是开挂???是不是图片PS过???确实PS过,用PS把以上5张图片合成一张图片,运行数据确确实实没问题。信不信???不信上机运行一下[笑哭]
解析:

来看看蓝桥杯真题的题目 ,笔者由浅入深逐一剖析;

 X星球特别讲究秩序,所有道路都是单行线。一个甲壳虫车队,共16辆车,按照编号先后发车,夹在其它车流中,缓缓前行。
路边有个死胡同,只能容一辆车通过,是临时的检查站,如图【p1.png】所示。
 
X星球太死板,要求每辆路过的车必须进入检查站,也可能不检查就放行,也可能仔细检查。
如果车辆进入检查站和离开的次序可以任意交错。那么,该车队再次上路后,可能的次序有多少种?
为了方便起见,假设检查站可容纳任意数量的汽车。
显然,如果车队只有1辆车,可能次序1种;2辆车可能次序2种;3辆车可能次序5种。
现在足足有16辆车啊,亲!需要你计算出可能次序的数目。
这是一个整数,请通过浏览器提交答案,不要填写任何多余的内容(比如说明性文字)。

在这里插入图片描述
链接: 以下的代码优化基于此文章

先看一下低效代码

public class 出栈顺序_未优化 {

    // 不用管出站后车的数量和顺序,因为进站顺序和出站顺序已经决定出站时的排序
    static int f(int a, int b) {// a是等待进站的数目,b是站中的数目
        if (a == 0)// 此时已全部进站,出站时的顺序只有一种
            return 1;
        if (b == 0)// 此时车站为空,只能让车进站
            return f(a - 1, 1); //左侧候车区a辆车开一辆到车站 车站内就有一辆车
        // 有两种走法:1、让一辆车进站(候车区车减1 车站车加1) ;2、让一辆车出站(候车区的车不懂 车站内的车减1)
        return f(a - 1, b + 1) + f(a, b - 1);   //这两种走法形成所有的可能
    }

    public static void main(String[] args) {
        System.out.println(f(16,0));	//初始状态 车站内无车
    }
}
//答案是:35357670

本题数据量不大,只有16辆车,有35357670种可能性,幸好本题只有16辆车,如果有50辆车呢????好像有点难办!!!
上文提到的缓存池法只针对一个参数,对应一维数组,该递归函数有两个参数,可以设置一个二维数组作为缓存池。难点也在于多个参数可以设置一个多维数组作为缓存池

以下为优化代码(略作修改,可以自定义车的数量):

import java.util.Scanner;

public class 出栈顺序 {
    static double[][] cache = new double[500][500]; //缓存池 根据世纪需要可调节缓存池大小
    static int n;
    public static void main(String[] args) {
        Scanner sc= new Scanner(System.in);
        System.out.println("输入指定的车辆数n:");
        n=sc.nextInt(); //指定输入的车数
        long start = System.currentTimeMillis();
        System.out.printf("%.0f\n",f(n,0));
        long end = System.currentTimeMillis();
        System.out.println("优化后的递归运行耗时"+(end-start)+"ms");
    }

    private static double f(int a, int b) {
        //a代表左边车道剩余的车 b代表栈中的车
        if(a==0)
            return 1;//左车道无车
        if(b==0)
            return f(a-1,1);  //栈中无车 需要来一辆
        if(cache[a][b]!=0)
            return cache[a][b];
        double x = f(a-1,b+1);
        cache[a-1][b+1]=x;
        double y = +f(a,b-1);
        cache[a][b-1]=y;
        return x+y;

    }
}

输入50辆车的测试用例:
在这里插入图片描述
最后来总结一下:
递归并非是一个低效算法,也并非非常难,只要先掌握最基本的递归,加上适当的优化方法,递归写出来的算法完全不输于其他算法。递归原本是一种以空间换时间的算法,花费极小的空间,代价是花费大量的时间,加入了缓存池后需要额外增加数组的空间开销,但大大节省了时间,递归的易用性加上优化后的高效性,足以写出不输任何方式的算法。希望读者阅读完本文后有所收获。最后提一下用缓存池法可能会遇到ArrayIndexOutOfBoundsException(数组越界异常),这个时候把数组的大小修改一下,不放心的话直接改为1k或1w即可解决问题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值