写在前面
很早就接触过递归,印象中递归只是使得问题的描述变得简单并不能优化算法反而会占用一部分系统资源,因此一直在避免使用递归。
闲来无事特意又查了查关于递归的资料,算是有了新的收获吧,对递归又有了新的理解。
于是写了这么一篇文章,一则记录一下,方便自己回头看;二则,如果能够帮到读者哪怕一点点,也算是功德圆满了。
什么是递归
我们先来假设这样一个场景:
有一个n层的大超市,所有的信息只能在相邻的楼层间进行传递,顶层为Boss的房间,底层(一楼)为统计长(不要较真这个职务)的房间,Boss一挥手,给统计长发布命令要查账,因此首先要将Boss的命令传递到一楼(传递的过程中每层统计本层的数据),然后一楼的统计长发布统计命令,每层之间逐层向上的将本层信息和下层信息汇总后交给上层,最后将最终结果交给Boss
整个过程的步骤抽象后如图所示:
我们再看一个例子:
用递归求斐波那契数列的某一项fab(n)的问题,很容易用代码将其进行实现
public static long fab(int n) { if (n <2) { return n; } return fab(n - 1)+fab(n-2); }
为了方便描述,假设我们求一下fab(5)(类比Boss的要求),
很显然fab(5)=fab(4)+fab(3), fab(4)=fab(3)+fab(2), fab(3)=fab(2)+fab(1),
fab(2)=2,fab(1)=1(类比底层的统计长)为已知条件,
那么通过fab(2)和fab(1)很容易将fab(5)求出来
我们来总结一下两个例子的共同点:
1、有一个“顶层Boss要求”,说明目标的是什么
2、有一个“底层统计长”,可以处理目标要求
3、有“询问”和“回调”的过程
我们可以给递归做这样的定义:
当一个函数,直接或间接的,用它自身来定义时就称为是递归的
递归的几条基本法则
由前面总结的两个例子的共同点,我们可以看出递归的几条基本的法则:
1、基准情况。必须总要有某些基准情况,它无需递归就解出来
2、不断推进。对于那些需要递归求解的情况,每一次递归调用都必须要使状况朝向一种基准情况推进
3、设计法则。假设所有的递归调用都能进行
4、合成效益法则。在求解一个问题的同一案例时,切勿在不同的递归调用中做重复性的工作
此处拿“超市”的例子对第四条做特殊解释:
因为相邻的楼层之间才能传递信息,那么在第一次向下将查询命令传递给统计长的时候,必须要保存层与层之间的关系(返回地址),而且还有保存本层统计的信息(参数,局部变量),以保证回调时目标的准确性和信息的准确性。
我们需要大量的空间去保存这些信息,如果“楼层过多”,也许空间就不够用(StackOverflowError)
因此没有经过优化处理的递归不仅不会降低反而会提高算法的时间复杂度(进栈弹栈需要花费时间)和空间复杂度(保存信息需要大量空间)
什么是尾递归
在解释什么是尾递归之前,我们再来看一下前面“超市”的例子:
每层的统计员都必须得到底层的统计长的命令才能统计,这就让问题变得不那么灵活。
如果让统计长住在顶层,并且给底层的统计员安装一部可以直接连接顶层的电话,那么如果Boss再想查账,那么就容易了许多。
命令在顶层就已经传到了统计长的“手里”,此时逐层向下统计,最终底层将结果直接“发给”顶层的统计长,统计长再给Boss。
整个过程的步骤抽象后如图所示:
从这个优化后的例子不难看出,尾递归就是一种优化后的递归。
我们可以给尾递归做这样的定义:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的
也就是说当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
我们来看一下修改成尾递归后的求斐波那契数列的代码:
public static long fabTail(int n, long acc1,long acc2) { if (n == 0) { return acc1; } return fabTail(n - 1, acc2, acc1+acc2); }
我们再来求一下fab(5,0,1)
很显然fab(5,0,1)=fab(4,1,1)=fab(3,1,2)=fab(2,2,3)=fab(1,3,5)=fab(0,5,8)=5
直接在最开始就处理,一直到基准为止,而不是先找到基准再回调处理
我们再来看一下优化后的“超市”问题中需要保存的东西:
因为最后的信息在底层直接传动到了顶层,不需要再“逐层回调”,因此不需要保存每一层统计信息,以及层与层间的关系,只需将下一层需要的信息传递到下一层即可。
即只要保存下一层需要的信息即可。
那么在理论上来说,楼层可以“无限多”,因为保存的信息所占用的空间是一样的。
写在末尾
但是如果编译器没有对尾递归进行优化的话, 或者对尾递归支持得不太好的话, 使用尾递归还是会出现递归里说的那些问题。
很遗憾Java虚拟机就是这样。。。。。。。而我又是一个Java程序猿(快成狗了)
因此关于尾递归的优化问题,以及continuation的知识,此处就不做过多的阐述,有兴趣的读者可以深挖一下。