您的位置:首页 > 其它

传智播客-递归(1)-jvm中保存运行方法的原理及递归方法设计要点

2009-12-18 23:29 330 查看
这是传说中的张孝祥老师的某一节讲课内容,以下内容都是基于张老师一篇单讲递归的25页文档(小菜鸟景仰中。。。)来写的(美其名曰:归纳总结:P)。

我前面写的二叉树其实已经用到了递归,也有较细的讲解,不过张老师的这个更为详实更加全面(小菜鸟继续景仰中。。。),而且对如何用递归算法解决实际问题有更具指导性和实用性的建议。

1、递归的概念和作用

概念很简单,自调用或自循环;作用嘛,说起来也简单,可以解决一般的循环问题(不过一般能用循环解决的就尽量用循环,因为递归比较耗资源),也可以解决一些常规方法难以解决的问题,例如经典问题--汉诺塔(Hanoi),总的来说,是可以解决一环套一环的问题,而且这每个环都是同质的。

递归常见的错误是“堆栈溢出错误”(StackOverError)。

这里可以引申出一个问题,在方法相互调用的过程中,当子方法执行完毕之后,jvm如何知道返回上一级方法的正确位置再继续执行呢?当程序在运行期间每调用一个方法时,jvm都会为其分配一块私有空间,这个私有空间称为帧空间,帧空间中存储了这个方法中的局部变量和该方法内部的异常处理的描述信息,以及当前方法中即将要执行的代码的位置,jvm就是根据这个位置来决定下一步要执行的代码,并在帧空间中实时动态地修改这个位置的值(道理能懂,就是不清楚这个代码位置如何保存在jvm里,先分割代码语句?再用数组或大牛们定义的某种标记标识这些代码句??方法调用时就在帧空间里保存这个代码的数组下标或标记信息???瞎猜的。。),当这个方法返回后,jvm就会回到上一级方法的帧空间检索这个位置,继续运行,而子方法的空间就会被jvm回收。

jvm用方法调用栈来跟踪线程中的一系列方法调用过程和管理方法的帧空间,当一个新方法被调用时,这个方法的帧空间就会被jvm压入栈顶。假设方法的调用线索为A-B-C,当C正在运行时,方法调用栈结构图如下(文档截图):



如果C方法一直没有返回,方法调用栈就一直保持上面的状态,如果C方法返回了,C的帧空间就会被抛出,B则被置于栈顶。

所以,即使递归中的自调用方法所占的帧空间很小,但是一直递归下去,只创建帧空间,却没有被释放的,就会耗光内存空间,最终抛出“堆栈溢出错误”。

在实际应用中,有一种间接递归需要谨慎,即A调用B,B调用C,而C又调用了A,这种递归不是很明显,但是也很容易耗尽系统资源。

2、递归方法设计要点(当然是张老师总结的,俺还木有这个实力)

(1)在编写递归方法的同时就假设这个方法已经可以被调用了,并且要调用这个方法自己去解决从当前大问题中抽取出来的同一种性质的更小问题。写递归方法时,不要去关心和琢磨抽取出来的较小的问题是具体如何处理的细节和过程(个人以为,这句是递归方法设计最重要的地方,或者说是最具有指导意义的一点,我写后面的汉诺塔时,没有看代码,根据这个指导方针,很快就写出来了,倒是后面改进汉诺塔的图形显示时遇到了点小麻烦。。),而是认为当前正在编写的递归方法正好就处理这个问题。

(2)在递归方法内部调用自己时,需要计算出传递给这次调用的参数,也就是要计算出下一次递归调用所要解决的问题域。(好像还看到过一个说法,递归方法的参数,有时也可以叫做自变量,因为递归调用自身时,使用的参数一般就是当前递归方法传入的参数变量,当然递归调用之前由的参数会重新处理一下)

(3)在一定的条件下必须终止继续向下进行递归调用,使得子方法调用能够一级级返回到主方法,这个终止递归调用的条件叫做“递归头”。每个递归方法都必须有个“递归头”,即必须限定一个递归结束条件来终止继续向下进行递归调用,否则,这个递归方法运行时必然会出现错误。另外,对于一个递归方法,即使在其中设定了“递归头”,程序运行时仍有可能会用尽系统资源,所以,对于一个递归方法,还应该人为限制递归调用的次数,不能让递归次数过大。(简单地说,就是上面讲的自变量变化是可以穷尽的)

下面附赠一段递归的入门小代码:
public static void toBinary(int data, StringBuilder result){
int temp = data%2;
int dataN = data/2;
System.out.println(temp);
if(dataN != 0){
binaryX(dataN, result);
}
result.append(temp);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: