递归算法的描述与实现

递归算法在C/C++程序设计巾硇描述与实现

[摘要]递归是函数实现的一个很重要的环节,对许多复杂的问题,递归能提供简单、自然的解法。本文在对递归的概念进行介绍的基础上,重点讨论了递归的程序设计方法,并分析了递归函数的调用和回溯过程。
[关键词]递归 函数调用 C/C++

1.引言
递归是计算机科学中一种强有力的问题求解方法,用递归算法编写的程序结构清晰,具有很好的可读性,正确性容易验证。但由于递归的设计思想比较巧妙,特别是对于规模较大的问题,掌握递归算法的设计分析和实现过程并非易事,因此,有必要对其进行深入探讨,分析其概念及设计方法和实现过程,以此加深对递归算法思想的进一步理解,学会正确应用递归解决实际问题的方法。
2.递归的概念
如果一个对象部分地由自己组成或者是根据自己定义的,则称这个对象是递归的;在程序设计中,若一个过程直接地或间接地调用自身,则称这个过程是递归的过程。在C/C++程序中,因为每次调用函数时,c ++语言都会为参数和局部变量分配新的存储区空间,因此函数调用它自身是可能的。这种类型的函数称为递归(recursive)函数。当函数调用它自身时,这个过程称为直接递归(direct recursion)。同样,函数能够调用第二个函数,反过来第二个函数也可以调用第一个函数。这种类型的递归称为间接(indirect)递归。
通常如下两种情况会用到递归:
(1)问题的定义是递归的。
许多数学上常用的概念是递归定义的。如阶乘函数的常见定义:
. f 1, 当n=0时
In (n一1)!, 当n≥1时
这种定义方法是用阶乘函数自身定义了阶乘函数。由于n!和(n一1)!
都是同一个问题的求解,因此可将n!用递归函数来描述。程序代码如下:
【例1】用递归函数编程求n的阶乘n!。
long factorial(int n)
(
1ong result=0;
if(O==n)
result=1;
else
result=n*lfactorial(n一1)j//调用自身
return result;
)
(2)问题的解法是递归的。
例如有序表上的二分查找过程:确定查找区间的中心位置,用待查找数据与中心位置上的数据比较,若两者相等则查找成功;否则若前者小于后者,则把查找区间定为原查找区间的前半部分,继续重复之前的查找过程;若前者大于后者,则把查找区间定为原查找区间的后半部分,继续重复之前的查找过程。在这里我们可以发现,对当前区间的查找过程与当前区间的前半部分或后半部分的查找过程是同一个问题的求解,因此可以将这一过程设计用递归函数来描述。
上述两种情况的求解过程中有一个共同的特点:都可以将待求解问题分解为形式更加简单的子问胚,而子问题的求解方法与原问题的求解方法又是相同的。有时一个问题的求解过程可以化为较小问题的求解过程,而较小问题的求解过程又可化为更小问题的求解过程,依次类推。这种有规律地将原问题逐渐化小的过程,并且求解小问题的方法与求解大问题的方法相同,则称为递归求解过程。由于在递归过程中,求解的问题越化越小,最后不需要再向下递归而能够直接得到一个最小问题的解;然后再逐层向上返回,依次得到较大问题的解,因此最终得到原有问题的解。
3.递归的设计方法
能采用递归描述的算法通常有这样的特征:把规模大的、较难解决的问题分解成规模较小的、易解决的同一问题。规模较小的问题又用同
样的方法分解成规模更小的问题,并且小到一定程度可以直接得出它
的解,从而得到原来问题的解。特别地,当规模N=I时,能直接得解。
当一个问题存在上述构成递归的特征时,我们就可以考虑利用递
归进行处理。由此我们分析适宜用递归求解问题的充要条件是:其一,
问题具有某种可借用的类同自身的子问题描述的性质;其二,某一有限
步的子问题有直接的解存在。
当一个问题存在上述两个基本要素时,设计该问题递归算法的方
法是:
(1)设计递归公式。即将一个问题化解为一个或多个子问题求解,
且子问题和原问题具有相同的解法。例如,前面【例1】的n!可化解为n
(n一1)!,其中,子问题n一1)!和n!的解法相同。
(2)设计递归出口。即递归终止条件(边界条件)。递归最后一级的
调用必须不能再进行递归,递归函数必须返回。不能无穷递归。
在通常情况下,递归调用都是要受到条件控制的,而且在被调用的
过程中,会对调用条件进行有规律的修改,即每递归一次要使递归趋于
结束,直到满足边界条件,返回边界值,结束递归;然后按照原来的路径
逐层返回,求出原问题的解。由此可知,递归算法设计的关键在于递归
描述(递归公式)和递归终止条件。
一般地,用递归方法进行“问题求解”时,具体需要依次进行下列三
个步骤。
(1)化解问题,求得算法
当问题化解成子问题后,有的可以写出一个迭代公式。如阶乘n!,
可写迭代公式f(n)=n f(n一1)。
有的可以归于一个操作的循环。例如,若将一个十进制数n转换为
二进制数输出,则可有下列循环:①输出n的2的余数(n%2);②将n
向右移一位或求n/2,若结果为O,输出后结束;若结果不为O,则转步骤
① 。程序代码如下:
【例2】将十进制数I1转换为二进制输出。
#include<iostream>
using namespace std;
void d2b(int n】;
intmain()
(
d2b(147);
return 0:
}
void d2b(int n)
(
if((0==n)lI(1==n))
cout<<n;
else 、
f
d2b(n/2);
cout<<n%2;//注意输出语句与递归调用语句的位置关系
}
}
程序运行结果:1001001 1
无论是上述的哪一种情况,我们都可以看出每次递归在规模上都
有所缩小(通常是减半),而且相邻两次递归之间有紧密的联系,前一次
要为后一次做准备(通常前一次的输出就作为后一次的输入)。
(2)规划递归路线
有些问题无须返回值,它要求完成相应的递归操作,类似递推,称为递归过程。无论是递归过程还是递归函数,递归路线可以有一条或多
条。如【例1】中的F orial函数,自身调用语句只有一条,也就是递归路
线只有一条。若递归函数或过程中自身调用语句不止一条,则可形成多
条递归路线。但是每次递归的时候只有一条路线起作用。如二分查找用
递归算法实现时,由于待查找数据与中心位置数据的比较结果不止一
个,所以就会形成多条递归路线,但每次调用只有一条路线会被执行
到。
需要说明的是,由于递归调用是在栈内存空间中完成的,而栈的工
作原理是先进后出,因此当递归路线中需要输出时,输出语句放在递归
函数调用语句之前和之后次序是刚好相反的。如【例2】函数d2b中,若
语句“eout<<n%2;”,放在递归调用语句“d2b(11/2);”之前,则递归后
“cout<<n%2;”从左至右输出最低位到最高位的值。显然,这样输出次序
是颠倒的。应将语句“cout<<n%2;”放在递归调用语句之后,利用栈的先
进后出原理,从而输出正确的次序。
(3)确定形参,设计递归终止条件
在问题的规模极小时必须直接给出解答而不再进行递归调用,因
而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),
无条件递归调用将会成为死循环而不能正常结束。递归过程或递归函
数的参数值在递归过程中必须是按规律变化的,且参数值的增/减方
向应与递归终止条件相匹配,这样才能控制递归调用。如【例2】递归函
数d2b中的形参n,每次递归调用时n值减半,直到n等于0或1为止,
递归终止。
一般递归函数设计的格式为:
说明:若递归路线为多条,则else分支不止一个。
4.递归过程
递归函数在实现过程中需多次进行自我调用,因而正确理解递归
调用及回溯过程是十分重要的。
由函数调用的内部机制我们知道函数调用是在栈内存空间中完成
的,栈的工作原理是先进后出。当调用一个函数时,整个调用过程分为
三步进行:调用初始化、执行函数调用、调用后处理。高级程序设计语言
利用堆栈保存递归函数调用时的信息,系统中用于保存递归函数调用
信息的堆栈称为运行时栈。
递归函数调用时,系统的运行时栈会将函数的返回地址、本次函数
调用时的实参值、被调用函数的局部变量值三种信息构成一个工作记
录存人栈中,在每进入下一层递归调用时就建立一个新的工作记录,并
把这个工作记录进栈成为运行时栈的新栈顶;每退出一层递归调用,即
函数调用返回时,系统运行时栈需要保存函数的返回值、释放实参和局
部变量的内存空间,释放栈空间,然后按照之前所保存的返回地址返
回,完成一个工作记录的退栈。因为栈顶的工作记录必定是在当前运行
层的工作记录,所以栈顶的工作记录称为活动记录。
由于函数的地址是系统动态分配的,调用函数的返回地址因此也
是动态变化的,不好给出具体数值,所以我们仅以递归函数调用时形参
的内存地址作为考查对象,来分析一下递归函数的调用和回溯过程。
为了能看清楚递归函数在运行过程中调用和返回的情况,这里将
【例l】中函数factorial的代码修改如下(斜体阴影部分),其中begin、end
两个静态局部变量分别表示递归函数的调用序号和返回序号,用&n获
取参数n的地址。
#include<iostream>
using namespace std;
long factorial(int n);
intmain()
{
cout<<”4『一”<<factorial(4)<<endl;
return O:
}
long factorial(int n)

long resuh=0;
retum resuk;
)
程序运行结果如下:
调用序号1 O012FF2C factoriall(41
调用序号2 0012FECC factorial(3)
调用序号3 O012FE6C factorial(2)
调用序号4 0012FEOC factorial(1)
调用序号5 O012FDAC factorialf0)
返回序号l 0012FDAC factorial(0)=l
返回序号2 0012FEOC factorial(1)=l
返回序号3 0012FE6C factorialf2)=2
返回序号4 0012FECC factorial(3)=6
返回序号5 0012FF2C factorial(4)=24
4『_24
由以上运行结果可见,尽管函数factorial递归调用时的形参都是
n,但每次调用系统会为其分配不同的内存空间。程序第一次调用 一
torial函数计算41,即调用函数factorial(41,形参n接受传递的实参值4,
因n=4,不等于0,故执行“resuh=4*factorial(3);”,并未直接得到factorial
(4)的结果,而是要继续调用factorial(3)。同理,在进行函数factorialO)调
用时,又因为要执行“resuh=3*factorial(2);”而无法直接得到factorial(3)的
结果,需继续调用函数factorial(2),依次类推。直到进行函数factorial(0)
调用时,形参n=O,故执行result=l,继而执行“retum resuh;”,到此时,才
第一次得到了函数factorial的返回结果,但此时得到的结果factorial(O),
是5次函数调用中最后一次的调用结果,而第一次函数调用factorial(4)
将会在最后得到运行结果。由此证明,递归函数是按照“后调用先返回”
的原则进行调用和回溯的。
结合上述例子,递归算法的执行过程是不断地自调用,直到到达递
归出口才结束自调用过程;到达递归出口后,递归算法开始按最后调用
的过程最先返回的次序返回;返回到最外层的调用语句时递归算法执
行过程结束。
5.结束语
在选择递归结构进行程序设计时,要考虑到“可理解性和效率”之
间的关系。递归的能力在于能用有限的语句来定义对象的无限集合。用
递归思想写出的程序往往十分简洁易懂。但需要注意的是,递归是一种
代码简短的程序设计方法,但它执行起来效率并不高,因为递归每次调
用都要进行调用初始化、执行函数代码、调用后处理这三步,不仅耗费
机时,而且也占用较多的栈空间,递归次数过多容易造成栈溢出。所以
如果采用递归算法设计程序,还需要控制问题的规模。

    原文作者:递归算法
    原文地址: https://blog.csdn.net/jie1991liu/article/details/8167115
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞