递归算法探讨
递归在计算机科学和数学中是一个很重要的工具, 它在程序设计语言中用来定义句法, 在数据结构中用来解决表或树形结构的搜索和排序等问题。另外, 递归在计算方法、运筹学模型、行为策略和图论的研究中都得到了广泛的应用。
1、 递归的概念
若一个对象部分地包含它自己, 或用它自己给自己定义, 则称这个对象是递归的; 在程序设计中, 若一个过程直接地或间接地调用自己, 则称这个过程是递归的过程。在定义一个过程或函数时出现了调用本过程或函数的成分, 即调用自己本身, 称之为直接递归;若过程或函数P调用过程或函数Q , 而 Q 调用P, 称之为间接递归。对于“问题定义是递归的, 数据结构是递归的, 问题解法是递归的”这3种情况, 都可以采用递归方法来处理。
2、 递归算法的本质
递归算法的本质是把一个大型复杂的问题层层转化为若干与原问题相似的规模较小的问题来处理, 当规模小到一定程度时, 可以直接得出它的解, 这样通过递推就可得到原来问题的解。递归调用的次数必须是有限的, 必须有递归结束的条件。递归算法的执行过程分为两步, 第一步是从目标出发追溯到源头, 称为回溯。第二步是从源头逐步回代达到目标, 称为递推。由于存在递推, 在回溯时, 必须保留其返回的地址与参数, 使程序能够返回到调用处继续执行, 这一步是系统通过设置栈来实现的, 程序设计者无需对栈进行管理。
3、递归算法的设计
适宜用递归算法求解的问题的充要条件是: 问题具有某种可借用的类同自身的子问题描述的性质; 某一有限步的子问题有直接的解存在。递归算法的设计, 通常有以下3 个步骤:
1、分析问题, 设计递归公式将一个问题化解为一个或多个子问题求解, 且子问题和原问题具有相同的解法。
2、设计递归结束条件, 控制递归,递归最后一级的调用必须不能再进行递归。
3、确定参数,设计递归函数递归过程或递归函数的参数值在递归过程中必须是按规律变化的,且参数值的增或减方向应与递归终止条件相匹配, 这样才能控制递归调用。一般递归函数设计的格式为:
if (递归结束条件)
return (结束递归时的返回值);
else
return (递归表达式);
4、递归算法的实例
例1: 用递归函数编程求n 的阶乘n!。阶乘函数的递归定义如下: n! =n×(n-1)! (n> 0)。这种定义方法是用阶乘函数自身定义了阶乘函数。由于n!和(n-1)!都是同一个问题的求解, 因此可将n!用递归函数来描述。
程序代码如下:
Long f( int n) {
if ( n = = 0 )
return 1; //递归的终止条件及相应的操作
else
return f (n-1) ; //递归调用 }
例2: 中序遍历二叉树的递归算法。
void Inorder ( BTreeNode BT ) {
if ( BT ! = NULL ) {
Inorder ( BT - > lchild) ;
Visit (BT ) ;
Inorder ( BT - > rchild) ; } }
5、递归算法的执行过程分析
递归的执行依赖系统堆栈的支持, 递归的执行过程主要分为两步, 回溯(逐层深入递归调用) 和递推(层层向上递归返回) , 在回溯时需要做的工作有: ①进行断点保存, 局部变量、形式参数保存。②控制流程转向递归调用的入口。
在本次递归调用结束后向上层调用返回时需要做的工作有: ①保存本次调用的函数结果, 恢复调用函数时的局部变量和形式参数。②根据递归调用时将控制流程转回到调用函数中递归调用的下一行代码处继续执行。
综上所述, 递归算法的执行过程是不断地自调用, 直到到达递归出口才结束自调用过程;到达递归出口后,递归算法开始按最后调用的过程最先返回的次序返回;返回到最外层的调用语句时递归算法执行过程结束。
6、递归算法的非递归化
递归算法在执行时, 存在多次进栈和出栈, 流程的跳转和返回, 甚至会出现多次重复计算, 从而影响执行效率。还有一些高级程序设计语言没有提供递归的机制和手段。因此, 有些时候将递归算法非递归化是有必要的。非递归化最重要的是理解递归的执行过程。对于一般的递归算法,可以利用以下两种方法对其进行非递归化。
1、 尾递归的非递归化
如果递归调用语句是函数的最后一条执行语句,则称这种递归调用为尾递归。当递归调用进入内层时,外层上与各形式参数对应的实际参数值和返回地址都会被编译系统自动保存下来,以备返回时使用。对于尾递归,调用返回时,其后已没有执行语句了。因而外层的实际参数值不会再用到,故没有必要保留。此外, 由于递归调用语句是最后一条可执行语句,返回地址肯定在函数末尾,故其返回地址也没有必要保留下来。对于这种情况,关键是从递归调用出发,从上而下递归到底, 找到递归的终止条件, 然后用循环实现递归算法的非递归化。
例3: 求n 的阶乘n!的递归算法的非递归化。
例1中给出了求n的阶乘n!的递归算法,从上而下递归:
f(n) = nf(n-1) = n(n-1)f(n-2) = n(n-1) (n- 2) f (n- 3) : : f (0)
f (0) = 1。
设最终结果用f表示, 由递归的终止条件“n=0时, 结果为1”知, f的初始值= 1。由此,可从下而上地用循环实现求f(i),其中i从1到任意正整数n,f随着i的变化而变化, 其非递归算法如下:
Long f(int n) {
int i;
long f = 1;
for( i=1; i<=n; i++)
f = f*i;
return f; }
类似的情形很多, 如求2个正整数的最大公约数和求Fibonacci数列等。
2、 非尾递归的非递归化
如果递归调用语句不是函数中的最后一个语句, 则称该递归调用为非尾递归。对于非尾递归调用中的入口地址, 计算机隐含地自动设置堆栈, 保留调用入口地址, 供递归返回使用。而用非递归方法, 堆栈是人为设定, 显现在程序中, 功能与递归算法相同。
例4: 中序遍历二叉树。
由二叉树的递归算法的执行过程知, 在二叉树非空时, 首先访问根的左子树, 再访问根, 最后访问根的右子树; 访问根的左子树时, 先要访问左子树根的左子树, 再访问左子树的根, 其次再访问左子树根的右子树⋯⋯, 如此递归下去, 一直到树的最左下结点被访问(向左下搜索时, 将当前结点压入系统栈中保存, 以便向上回退时调出) , 然后访问最左下结点的父结点, 通过弹栈获得最左下结点的父结点, 然后处理该父结点的右子树, 以此类推, 循环直到整棵树访问完毕。根据上述对递归执行过程的分析, 其对应的非递归算法为:
void Inorder ( BTreeNode BT )
{
if ( ! BT ) return ;
Stack S = Init-Stack () ;
while ( ! Empty-Stack(S) )
{ while(BT) //当指针BT非空时入栈
{Push (S,BT);
BT = BT-> lchild;}
Pop(S,BT) ;
Visit(BT);
BT = BT->rchild;
} //end while (Empty-Stack ( s) )
}//end InOrder ()
7、结束语
递归算法具有代码简洁, 思路清晰的优点, 是设计算法的强有力工具。一般而言, 递归程序的执行效率低于非递归程序, 但非递归算法往往难于编写, 容易出错; 理论上, 递归算法都可以转化为非递归算法, 但存在一些算法很难非递归化, 如复杂的间接递归, 因此, 要根据问题需求及软件和硬件的环境等具体情形选择递归还是非递归。