为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

递归算法详解

2013-12-26 50页 doc 1MB 34阅读

用户头像

is_305019

暂无简介

举报
递归算法详解递  归 递  归 冯文科 一、递归的基本概念。 一个函数、概念或数学结构,如果在其定义或说明内部直接或间接地出现对其本身的引用,或者是为了描述问题的某一状态,必须要用至它的上一状态,而描述上一状态,又必须用到它的上一状态……这种用自己来定义自己的方法,称之为递归或递归定义。在程序设计中,函数直接或间接调用自己,就被称为递归调用。 二、递归的最简单应用:通过各项关系及初值求数列的某一项。 在数学中,有这样一种数列,很难求出它的通项公式,但数列中各项间关系却很简单,于是人们想出另一种办法来描述这种数列:通过初值及 与前面临近几项...
递归算法详解
递  归 递  归 冯文科 一、递归的基本概念。 一个函数、概念或数学结构,如果在其定义或说明内部直接或间接地出现对其本身的引用,或者是为了描述问题的某一状态,必须要用至它的上一状态,而描述上一状态,又必须用到它的上一状态……这种用自己来定义自己的方法,称之为递归或递归定义。在程序设计中,函数直接或间接调用自己,就被称为递归调用。 二、递归的最简单应用:通过各项关系及初值求数列的某一项。 在数学中,有这样一种数列,很难求出它的通项公式,但数列中各项间关系却很简单,于是人们想出另一种办法来描述这种数列:通过初值及 与前面临近几项之间的关系。 要使用这样的描述方式,至少要提供两个信息:一是最前面几项的数值,一是数列间各项的关系。 比如阶乘数列 1、2、6、24、120、720…… 如果用上面的方式来描述它,应该是: 如果需要写一个函数来求 的值,那么可以很容易地写成这样: int f(int n) { if(n==1) return 1; return n*f(n-1); } 这就是递归函数的最简单形式,从中可以明显看出递归函数都有的一个特点:先处理一些特殊情况——这也是递归函数的第一个出口,再处理递归关系——这形成递归函数的第二个出口。 递归函数的执行过程总是先通过递归关系不断地缩小问题的规模,直到简单到可以作为特殊情况处理而得出直接的结果,再通过递归关系逐层返回到原来的数据规模,最终得出问题的解。 以上面求阶乘数列的函数 为例。如在求 时,由于3不是特殊值,因此需要计算 ,但 是对它自己的调用,于是再计算 ,2也不是特殊值,需要计算 ,需要知道 的值,再计算 ,1是特殊值,于是直接得出 ,返回上一步,得 ,再返回上一步,得 ,从而得最终解。 用图解来说明,就是 下面再看一个稍复杂点的例子。 【例1】数列 的前几项为 1、 、 、 、…… 输入 ,编程求 的精确分数解。 : 这个题目较易,发现 ,其它情况下有 。如要求实数解的话,这基本已经可以写出递归函数了。但由于题目要求精确的分数解,还需做一些调整。设 ,则由递归关系,有 ,再约分化简,即得 。但发现一个问题:求出 时,需要返回两个整数:分子 与分母 ,而通常的函数只能返回一个整数。 这个问题一般有两类解决办法,一种是让求值函数返回一个结构体变量,这样就可以返回两个变量了(其实还可以不只两个呢);另一种是在求值函数的参数表中加入两个指针变量或引用变量,通过参数给带回数值。但由于后一种做法会使程序结构不清晰——返回值是由参数表得到的,因此我们使用前一种方法。 另外,在通过 得出 后, 就已经是最简分数了,无须化简。证明如下: 若 是最简分数,即说明 的最大公约数为1,即对任何 ,都有 与 不全为0,不防记 、 ,则有 只要 与 不全为0,且 ,就有 与 不全为0。因此对任何的 ,有 与 不全为0。 而对于 的情况而言,记 ,则有 由于 ,因此同样有 与 不全为0。 所以对任意 ,都有 与 不全为0,因此它们的最大公约数为1,即 是最简分数。虽然这是个要求 (即 )是最简分数的结论,但由于数列第二项为 ,是最简分数,因此可以证明第三项也是最简分数,同时也证明对所有的 ,求出的 就是最简分数,无须化简。 具体代码如下: // Exam1.cpp #include using namespace std; struct FS { unsigned long long q, p; }; FS f(int n) { FS r; if(n==1) { r.q=1; r.p=1; return r; } r=f(n-1); r.p=r.p+r.q; r.q=r.p-r.q; return r; } int main() { FS r; int n; cin>>n; r=f(n); cout<标准
是它能使问题的规模变小,且变小后的问题与原问题在本质上是一样的。当找到递归关系后,我们的递归函数只须处理特殊情况与递归关系,不需要处理其它的信息——至于下一步的事情,就让下一步去做吧。 另一个需要考虑的事情就是递归函数的参数表,首先,在通常情况下,参数表都要使用变量参数,且递归函数中尽量使用局部变量——这会减少很多不必要的麻烦;其次,参数表中,大多都有一个表示当前在执行第几步的参数。 【例2】下图是一个有向图,输入 ,打印 的所有路径。 分析: 仔细研究这个图的特点,发现以下规律:对任何结点 ,都可以走到 和 ,当然如果它们不超过9的话。由于要打印路径,因此需要保存查找过程中的部分路径信息。 可以做一个全局数组path[]来存储这个信息,由于结点0没有来路,且是所有路径的起点,因此记path[0]=0,递归函数负责填写之后的路径结点。 我们这样设计递归函数: 首先,这个递归函数的参数表中至少需要一个参数i,它的意义是表示现在在填路径中的第几个结点,而path[i]可以填的数,要么是上一个结点加1,要么是上一个结点加2,即path[i]=path[i-1]+1或path[i]=path[i-1]+2; 其次,特殊情况的讨论:我们要找的是终止到 的路径,因此若出现path[i-1]= 的情况,就说明已经找到路径,无须将当前层再填入结点,可以将path中的信息输出并结束函数了——这是递归函数的特殊情况出口; 第三、递归关系的处理:若还没到达结点 ,则填写本结点path[i],上文已说明, path[i]=path[i-1]+1或path[i]=path[i-1]+2,当然如果它们都不过9的话。将结点 填好后,说明路径向下走了一步,距离结点 更近了一步,问题规模已经变小。不要处理其它东西,直接递归,通过递归调用去填写i+1结点就可以了。 这里有处和【例1】不相同的地方,即path[i]是有两种可能可选的,我们的处理这这样的,先令path[i]=path[i-1]+1,然后递归调用,填写i+1结点,当这个调用返回时,说明所有path[i]为path[i-1]+1的路径都已经讨论完成了,再令path[i]=path[i-1]+2,再递归,当它返回时,整个函数执行完毕,形成正常的执行完成时的出口。 具体代码如下: // Exam2.cpp #include using namespace std; #define MAX 9 int path[MAX], N; int write(int i) { int j; for(j=0;j"; cout<>N; path[0]=0; f(1); system("pause"); return 0; } 【例3】我的画笔。 Windows中的画笔从Windows3.x时代开始就已经有了,虽然功能与Photoshop不能相比,但它小巧而迅速,一般的简单功能还是很方便的。 画笔中的填色工具是油漆桶,选定它,再指定一个颜色,在图片中一点,所有与这个点颜色相同且相连的象素就都被填充了。如下图示: 画笔的油漆桶工具填充的是一个叫“四连通”的区域,即它只从上、下、左、右四个方向向外扩展。 请编写程序,模拟油漆通工具。 输入文件:Exam3In.txt中有10行,每行是一个10字符的字符串,表示一个10*10的图象,不同的字符表示不同的颜色;之后的一行有两个用空格分开的整数,表示油漆桶点中的位置,再后面一行是一个字符,表示油漆桶的填充颜色。 输出文件:Exam3Out.txt,输出10行,每行10个字符的字符串,表示填充后的图象。 分析: 本题给了一个点的位置,查找所有与它“四连通”的点就成为本题的核心问题。我们的算法是这样的:先从这个起点出发,沿四个方向展开,看这“直接相连”的四个点是否可以填充,若有可以的,则再以这个点为中心,再向四个方向展开……直到所有可能展开的点都不能再展开了为止。 递归函数这样设计:首先它的参数表需要两个参数,表示本次从哪个位置点展开;其次,依次讨论它四个方向上相邻的点是否可填充——与起点颜色相同,如果可以,则填充,并以此点为中心,递归调用。 代码如下: // Exam3.cpp #include using namespace std; #define N 10 ifstream fin("Exam3In.txt"); ofstream fout("Exam3Out.txt"); char pic[N][N+1], c, p; int col, row; void fill(int i, int j) { if(i-1>=0 && pic[i-1][j]==p) { pic[i-1][j]=c; fill(i-1, j); } if(i+1=0 && pic[i][j-1]==p) { pic[i][j-1]=c; fill(i, j-1); } if(j+1>pic[i]; fin>>row>>col; fin>>c; p=pic[row][col]; pic[row][col]=c; fill(row, col); for(i=0;i using namespace std; #define M 100 ifstream fin("Exam4In.txt"); ofstream fout("Exam4Out.txt"); char tree[M]; int L; void run(int i) { if(2*i+1>tree; L=strlen(tree); run(0); return 0; } 【例5】以字符串形式给出一棵完全二叉树的先序遍历与中序遍历序列,编程输出用字符串形式表示的完全二叉树的结构。 分析: 先序遍历的首结点一定是整棵树的根,因此可以直接标在0处。在中序遍历序列中,它左边的结点构成左子树,右边的结点构成右子树,从而可以知道,左子树与右子树的结点个数,进而得到及左子树与右子树的先序遍历与中序遍历序列,递归查找所有子树的根即可。 为了得到整棵树的结构,设置全局数组tree[]来存储,全局数组s[]的意义是:s[i]存储整数表示第i层结点的起始下标。 代码如下: // Exam5.cpp #include using namespace std; #define M 100 ifstream fin("Exam5In.txt"); ofstream fout("Exam5Out.txt"); char pre[M], mid[M], tree[M]; int L, s[M]; void build(int layer, int ps, int pe, int ms, int me) { tree[s[layer]]=pre[ps]; s[layer]++; if(ps==pe) return; int i; i=ms; while(mid[i]!=pre[ps]) i++; build(layer+1, ps+1, ps+i-ms, ms, i-1); build(layer+1, ps+i-ms+1, pe, i+1, me); } int main() { fin>>pre>>mid; L=strlen(pre); s[0]=0; int i, j; i=1; j=2; while(s[i] using namespace std; #define MAX 100 int n, q[MAX], c; bool mark[MAX]; void write() { int i; char t[MAX]; memset(t, '.', MAX*sizeof(char)); t[n]='\0'; for(i=0;i>n; memset(mark, 0, MAX*sizeof(bool)); c=0; search(0); cout<
上讲的用栈计算的方法本质不同的方法。 在详细说明这个算法之前,需要首先明确这个算法用到的概念 1、单元:一个单元可能是用括号括起来的一个表达式,或是一个整数; 2、项:一个项是指由*与/连接起来的若干单元; 3、表达式:一个表达式是指由+或-连接起来的若干项。 要建立表达式树,需要三个函数互相调用的函数:一个是getunit,用于建立一个单元;一个是getexpr,用于建立一个项,另一个就是build,用于建立一个表达式。 getunit函数较易,如果字符串首字母是(的话,那么从它后面的字符开始用build建立一个表达式,这个表达式就是一个单元;否则,就处理一个整数; getexpr函数是建立在getunit之上的,它先用getunit建立一个单元,然后不停地考察之后地连接符号是不是*或/,若是,则不停地重复读连接符、建立另一个单元、建立连接的操作,直到连接符号不是*或/为止。 build函数是用于最终建立表达式的,它先用getexpr建立一个项,再用符号将剩余的各项连接成二叉树。 代码如下: // Exer.cpp #include using namespace std; struct NODE { int n; char c; NODE *left, *right; NODE() { left=NULL; right=NULL; } }; char s[100]; int cur; NODE *tree; void clear(NODE *root) { if(root==NULL) return; clear(root->left); clear(root->right); delete root; } void cal(NODE *root) { if(root->left!=NULL) { cal(root->left); cal(root->right); switch(root->c) { case '+': root->n=root->left->n+root->right->n; break; case '-': root->n=root->left->n-root->right->n; break; case '*': root->n=root->left->n*root->right->n; break; case '/': root->n=root->left->n/root->right->n; } } } NODE *build(); NODE *getunit() { NODE *a; if(s[cur]=='(') { cur++; a=build(); } else { a=new NODE; a->n=s[cur]-'0'; cur++; } return a; } NODE *getexpr() { NODE *a, *b, *c; a=getunit(); while(s[cur]=='*' || s[cur]=='/') { b=new NODE; b->c=s[cur]; cur++; c=getunit(); b->left=a; b->right=c; a=b; } return a; } NODE *build() { NODE *a, *b, *c; a=getexpr(); while(s[cur]!='\0' && s[cur]!=')') { b=new NODE; b->c=s[cur]; cur++; c=getexpr(); b->left=a; b->right=c; a=b; } if(s[cur]==')') cur++; return a; } int main() { cin>>s; cur=0; tree=build(); cal(tree); cout<n<0 又例如,Fibonacci(斐波那契)数列可递归定义为               0                      若n=0 Fib(n) =    1                      若n=1               Fib(n-1)+Fib(n-2) 若n>1      据此可以写出实现求n 的阶乘和求Fibonacci数列中第n项的递归算法,如算法21和 算法22所示。 long int fact(int n){                           //求非负整数n的阶乘   if(!n) return 1;                              //0!=1   else return n*fact(n-1);                  //n!=n*(n-1)! }//fact 算法21   求阶乘的递归算法 long int fib(int n){                           //求斐波那契数列中的第n个数   if(n<2) return n;                            //f(0)=0,f(1)=1   else return fib(n-1)+fib(n-2);         //若n>1,f(n)=f(n-1)+f(n-2) }//fib 算法22   求斐波那契数的递归算法      一般地说,每一个递归函数的定义都包含两个部分。 (1) 递归基础      对无需递归的规模最小的问题直接进行处理。 (2) 递归步骤      将一般情况的问题简化成一个或多个规模较小的同性质的问题,递归地调用同样的方法 求解这些问题,使这些问题最终简化成基础问题。      算法21的递归基础是n=0时,直接返回1(0!=1)。一般情况下,将fact(n)简化成规 模较小的问题fact(n-1),求出fact(n-1)后再与n相乘即求得了fact(n) 。      算法22的递归基础是n<2时,直接返回n(因为fib(0)=0,fib(1)=1)。一般情况下,将fib(n) 简化成两个规模较小的问题fib(n-1)和fib(n-2),求出fib(n-1)和fib(n-2)之后再求它们的和即可 求出fib(n)。      递归函数结构清晰,程序易读,而且它们的正确性易于证明,所以递归函数是程序设计 的有力工具。为理解递归函数是如何实现的,先考察任意两个函数之间相互调用的情形。 用高级语言编制的程序中,调用函数和被调用函数之间的链接和信息交换需通过栈来进行。      通常,当一个函数在运行期间调用另一个函数时,在运行被调用函数之前,系统需先完 成三件事:(1)将所有的实在参数、返回地址等信息传递给被调用函数保存;(2)为被调用 函数的局部变量分配存储区;(3)将控制转移到被调用函数的入口。而从被调用函数返回调 用函数之前,系统也要先完成三件事:(1)保存被调用函数的计算结果;(2)释放被调用函 数的数据区;(3)按被调用函数保存的返回地址将控制转移到调用函数。当有多个函数构成 嵌套调用时,按照“后调用先返回”的原则。上述函数间的信息传递和控制转移是通过“栈” 来实现的(这个栈一般称为系统工作栈),即系统将整个程序运行时所需的数据空间安排在 一个栈中,每当调用一个函数时,就为它在栈顶分配一片存储区,每当从一个函数退出时, 就释放它的存储区。所以,当前正在运行的函数的数据区必在栈顶。 一个递归函数的运行过程类似于多个函数的嵌套调用,只是调用函数和被调用函数是同 一个函数。因此,和每次调用相关的一个重要概念是递归函数运行的“层次”。假设调用该 递归函数的主函数处在第0层,则从主函数调用递归函数为进入“第1层”,从第i层递归调 用本函数为进入“下一层”,即第i+1层。在退出第i层递归时,应返回到“上一层”,即第 i-1层。为保证递归函数的正确执行,系统也应象在处理一般函数调用时那样,在系统工作 栈中为递归函数开辟数据存储区。每一层递归所需信息构成了一个“工作记录”,其中包括 所有的实在参数、所有的局部变量以及返回到上一层的返回地址。每进入一层递归,就产生 一个新的工作记录压入栈顶。每退出一层递归,就从栈项弹出一个工作记录。所以,当前执 行层的工作记录必定是工作栈栈顶的工作记录,称这个工作记录为“活动记录”,并称指示 活动记录的栈顶指针为“当前环境指针”(curptr)。图4从编译器的角度说明了阶乘函数的 递归实现。 2   汉诺塔问题      除了某些数学函数可用递归方式定义外,一些本身具有递归特性的数据结构,如二叉树 (见第五章)等也可递归地描述。另有一类问题,虽然问题本身没有明显的递归结构,但用 递归求解比较方便,例如汉诺塔问题。      例4   (n 阶Hanoi塔问题)设有A、B、C三个塔柱。在A柱上插有由小到大编号为1 到n的n个直径各不相同的圆盘,如图5(a)所示。现要求按以下规则将A柱上的n个圆 盘移到C柱上:1)每次只能移动一个圆盘;2)大盘不能压小盘;3)圆盘可以插在A、B、 C中的任一塔柱上。      可以用递归的方法求解此问题。      递归基础:当n=0时,不需要作任何操作。      递归步骤:当n>0时,将此问题分解成三个规模较小的问题: (1) 首先将A柱上面的n-1个圆盘移到B柱; (2) 再将A柱上最大的第n号盘移到C柱; (3) 最后将B柱上n-1个圆盘移到C柱的第n号盘之上。 如图5(b)(c)(d)所示。 据此可写出求解n(n≥0)阶Hanoi塔问题的递归算法(算法23)。为方便叙述该算法的执 行过程,人为地在每条语句前添加了编号。图6展示了调用函数hanoi(2,a,b,c)的执行过程。 void hanoi(int n,char x,char y,char z){//入口条件:n>=0 1. if(n>0){                                          //若n=0,则不需做任何动作,仅当n>0时 2.    hanoi(n-1,x,z,y);                            //先将n-1个盘从x柱经z柱搬到y柱    cout<<"Move Disk "<0但n<1 时,背包问题也无解,因为不放任何物品到背包里,其重量之和不会是正值。假如S>0且n ≥1,我们要求解背包问题有两种途径:一种是选择第n件物品放入背包,于是背包剩余的 容量为S-W[n](W[n]中存储wn ,即第n件物品的重量),可选择的物品是前n-1件。如果 knapn(s-w[n],n-1)有解,则knap(S,n)也有解,否则knap(S,n)无解,说明选择第n件物 品是错的。另一种是不选第n件物品,此时背包问题简化为knap(S,n-1),如果它有解,则 knap(S,n)也有解,如果它无解,则knap(S,n)也无解,从而背包问题可以递归地定义如下:                              TRUE       若S=0       knap(S,n) =       FALSE      若S<0或S>0且n<1                              knap(S-W[n],n-1) 或 knap(S,n-1)      若S>0且n≥1 上述递归定义是确定的,因为每次递归,n总减少1,S也可能减小W[n],所以递归若干次 之后,递归基础的条件(S≤0或n<1)必定成立,所以递归过程在有限步之后总能结束。      例5   用递归方法求解背包问题。 根据knap(S,n)的定义,容易写出背包问题的递归算法,如算法24所示。 const int MAXN=11;                            //假设最多只有十件物品 int w[MAXN]={0,3,5,6,3,7,1,2,4,9,8}; //假设物品的重量已存在w[1]...w[10]中 int knap(int s,int n){                      // s为背包的容量,n为可供选择物品的最大编号   if(s==0) return TRUE;                     //若背包已装满,则有解   if((s<0)||(s>0)&&(n<1))return FALSE;//若背包容量为负数或已无物品可选,则必无解      if(knap(s-w[n],n-1)){   //先试探将第n个物品装入背包中,若因此有解,则原题有解         cout<资料
[3]。应该指出,如果一个问题可用递归算法求解,则必定有非递归的算法, 因为至少可以将递归算法转换成等价的使用栈的非递归算法。然而,可能也有其它的非递归 算法。例如,背包问题也可以用非递归方法求解。用一个栈stk来模拟背包,用T记录放入 背包中的物品的总重量。从第N个物品开始,逐次检查第N个,第N-1个,…,第1个物品, 如果背包中尚有空间可放第i个物品,则将它放入背包(即将它的编号压入栈stk,并令T 增加W[i])。如果放不下或虽有空间但已无可供选择的物品,则从栈中弹出一物品的编号(即 将该物品从背包中取走),并从T中减去它的重量,然后从它的下一个(编号小1)的物品 开始,重新检查能否将它放入背包,这个过程一直进行到T中物品总重量等于S或是栈stk 的空且要检查的物品编号已小于1。结束时如T等于S则求得一解,在栈stk中的物品(编号) 即为所选物品。 算法25描述了用非递归方法求解背包问题的算法。 int knap(int t,int n){                   // t为背包的容量,n为可供选择物品的最大编号   Stack s; ETp e; int i,found=0;//若找到一解,found被置为1,初始时found的值为0   if (t==0) return TRUE;                      // 背包已满,问题有解   StackInit(s,MAXSIZE);                        // 假设MAXZISE大于n   while (!found&&((n>0)||!StackEmpty(s))){//若尚未找到解且有物品可选或栈非空      if (n<=0) {                                    //若已无物品可选,则表明先前的选择有误         Pop(s,n);                     //将最近选择的物品(将其编号置入n)从背包中取出         t+=w[n];                                     //背包的容量t因此增加w[n]         --n;                                           //既然不能选用物品n,就试选物品n-1      }      if (n>0) {                                     //如果还有物品可选         if (t>=w[n]) {                            //而且背包还能放得下            t-=w[n];                                  //就将物品n放入背包,t因此减少w[n]            if (!StackFull(s)){                  //若栈未满               Push(s,n);                            //将n入栈,即将物品n放入背包               --n;                                     //因此可选物品的编号小1               if (t==0) found=1;                //若背包已满,则求得一解            }            else exit(ERROR);                     //若栈已满,则出错         }         else --n;                                    //若背包放不下物品n,则尝试选择物品n-1      }//if(n>0)   }//while   if (found)                                        //如果找到一解,就将所选物品的重量输出      for (i=s.top; i>0; --i){         Pop(s,e); cout<0;--i){                   //first中存储的数逐渐向第n个斐波那契数靠近      temp=first; first=second; second+=temp;}//first移向下一个斐波那契数   return first;                         //for语句结束时,first已是第n个斐波那契数 }//Fibo 算法28   求斐波那契数的非递归算法      显然,也可以用图10来解释Fibo的执行过程。由于Fibo消除了递归,省掉了n次函 数调用的开销,所以它的效率更高。    尾递归函数是一种特殊的递归函数。如果一个递归函数的返回值是直接计算而得或恰是 一个递归调用自身而得到的返回值,那么这个递归函数就称为尾递归函数。换句话说,如果 递归函数中有一个递归调用(自身)的语句是这个函数的最后一个可执行语句,那么这个函 数就被称做尾递归函数。应该注意,最后一个可执行语句并不一定是程序中的最后一条语句。 例如,Fib 的返回值或者是first(若n=0),或者是递归调用Fib的返回值,所以它是尾递归 函数。从算法27中也可以看出。Fib函数中的递归调用语句是最后一个可执行语句。    尾递归函数是一类重要的函数,一方面它的效率一般较高,例如算法27的效率比算法 22的效率高。另一方面,很容易消除尾递归函数中的递归,使之成为非递归函数,从而不 需要用栈来保留每个递归副本的局部变量,例如算法28比算法27效率更高。      例6   用尾递归实现阶乘函数,并消除递归。      算法29和算法30是用尾递归实现阶乘函数的算法。算法31是求n的阶乘的非递归算 法。 long int newfact(int n){//入口条件:n>=0,把实质性工作交给Fact去做   return Fact(1,n);// }//newfact 算法29   另一种求阶乘算法 long int Fact(long int result,int n){//入口条件:n≥0,第1次调用时,result应为1   if(n<=1) return result;   return Fact(result*n,n-1); }//Fact 算法30   求阶乘的尾递归算法 long int Factor(int n){   //求n!(n≥0)的非递归算法   long int result=1; int i;   for(i=n;i>1;--i) result*=i;   return result; }//Factor 算法31   求阶乘的非递归算法      应该指出,并不是所有的递归函数都可用尾递归求解,也没有一般法则设计尾递归。尾 递归函数有时也未必比递归函数效率更高(例如,算法29并不比算法21效率高,因为它多 用了一个形式参数且递归次数并未减少),但容易根据尾递归函数消除递归,从而提高效率 和减少使用的存储空间。 5.5   递归与迭代      有一类问题,既可以用递归的方法求解,也可以用迭代的方法求解。所谓迭代(iteration) 意即重复。例如,求n个数a1 ,a2, …,an 的和用递归的方法求解,其思路是:若n=0,则和 为0,否则可将这n个数看成由两部分组成:前n-1个数和最后一个数an。用递归的方法求 出前n-1个数的和,再加上an,即可求出这n个数的和,见图11(a)。用迭代的方法求解,其 思路是:若n=0,则和为0,否则可将这n个数看成一串数:最后一个数an和它之前的n-1个 数。在累加和中依次加入an ,an-1 …,a1即可求出n个数的和,见图11(b). 算法32是求n个数的和的递归算法。算法33是相应的迭代版本。假设n个数已存储在 数组a的分量a[1],…,a[n]中。 float sum(int n){//求n(n≥0)个数的和的递归算法,假设这些数存放在数组a中   if(n==0) return 0;   return sum(n-1)+a[n]; }//sum 算法32   求和的归算法 float Sum(int n){//求个n(n>=0)个数的和的非递归算法,假设这些数存放在数组a中   float s=0; int i;   for(i=n;i>0;--i) s+=a[i];   return s; }//Sum 算法33   求和的非递归算法      从可读性上看,这两个算法都易于理解,但算法32不仅要调用sum函数n+1次,而且 在系统工作栈中要保留n+1个实在参数(从n到0)。相反,算法33只调用函数Sum一次, 且在系统工作栈中只保留实在参数n和局部变量s和i。因此迭代方法的效率比递归方法的 效率高。但不是所有递归算法都有相应的迭代算法,例如hanoi( )函数。尽管没有一般的递 归与迭代之间的对应(转换)法则,但有些较复杂的递归函数,仍可用迭代方法求解。      例7   用递归和非递归方法求二项式系数C(n,k)= C   (n≥k)。 根据数学知识,C(n,k)=n!/(k!×(n-k)!)。但用这个公式求C(n,k)是不适宜的,因为 C(20,1)=20,但20!是一个很大的数,可能不能用整型变量存储,所以要用C(n,k)的递归 定义:          1       若k=0 C(n,k) =      1       若k=n                C(n-1,k)+C(n-1,k-1)       若n>k>0 据此可写出求C(n,k)的递归算法,如算法34所示。 int com(int n,int k){                     //用递归方法求C(n,k) (n≥k≥0)   if(k==0) return 1;                      // C(n,0)=1   if(k==n) return 1;                      // C(n,n)=1   return com(n-1,k)+com(n-1,k-1);   // C(n,k)=C(n-1,k)+C(n-1,k-1) }//com 算法34   求组合数的递归算法 这个算法虽然避免了用公式计算时可能引起的溢出,但效率很低。例如,求com(5,1) 时,就调用了函数com(n,k) 九次,见图3。12。 为设计一个非递归的求C(n,k)的算法,先研究一下著名的杨辉三角形,如图13(a)所示, 其中第n行第k个数恰为C(n,k)。注意到C(n,k)=C(n,n-k),于是不妨假定k≤n/2。假设每行最 右边的1之后都是0,那么可以从第1行开始,逐行求出杨辉三角形的下一行。对第i行第 k(0≤k≤i)个元素,利用公式C(i-1,k)+C(i-1,k-1),即可依次求出a , a ,…,a , a (上标 [m]表示第m行),如图13(b)所示。因为要求的是C(n,k)(k≤n/2),所以对每一行只需求出最 右的k+1 个数即可。又因为不需要保存每一行的值,故可以只设一个有n+1 个元素的数组 a。由此可得用双重迭代求C(n,k)的非递归算法,如算法35所示。调用Com(5,3)时,算法执 行过程如图 13(c)所示。 int Com(int n,int k){                        // 用迭代方法求C(n,k) (n≥k≥0)   int i,j,*a=new int[n+1];                // 产生一个有n+1个元素的数组a   for(i=1;in/2) k=n-k;                            // C(n,k)=C(n,n-k)   for(i=1;i<=n;++i)                           // 逐行求C(i,k)      for(j=0;j<=k;++j)                        // 每行只要求最右边的k+1个C(i,j)         if((i-j)>=1)                            // 若i
/
本文档为【递归算法详解】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
相关资料
热门搜索
你可能还喜欢

历史搜索

    清空历史搜索