蓝桥杯 出栈顺序问题引发的思考以及递归的优化(缓存池)
关于递归的优化和思考
在我们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即可解决问题。