函数的递归调用与分治策略解读文档格式.docx

上传人:b****6 文档编号:19817589 上传时间:2023-01-10 格式:DOCX 页数:13 大小:24.66KB
下载 相关 举报
函数的递归调用与分治策略解读文档格式.docx_第1页
第1页 / 共13页
函数的递归调用与分治策略解读文档格式.docx_第2页
第2页 / 共13页
函数的递归调用与分治策略解读文档格式.docx_第3页
第3页 / 共13页
函数的递归调用与分治策略解读文档格式.docx_第4页
第4页 / 共13页
函数的递归调用与分治策略解读文档格式.docx_第5页
第5页 / 共13页
点击查看更多>>
下载资源
资源描述

函数的递归调用与分治策略解读文档格式.docx

《函数的递归调用与分治策略解读文档格式.docx》由会员分享,可在线阅读,更多相关《函数的递归调用与分治策略解读文档格式.docx(13页珍藏版)》请在冰豆网上搜索。

函数的递归调用与分治策略解读文档格式.docx

iostream.h>

intf(intx){

return(f(x-1));

}

main(){

cout<

<

f(10);

它没有规定递归边界,运行时将无限循环,会导致错误。

[步骤3]写出递归函数并译为代码将步骤1和步骤2中的递归关系与边界统一起来用数学语言来表示,即

N*(N-1)!

当N>

=1时

n!

=

1当N=0时

再将这种关系翻译为代码,即一个函数:

longf(intn){

if(n==0)

return

(1);

else

return(n*f(n-1));

[步骤4]完善程序主要的递归函数已经完成,将程序依题意补充完整即可。

//ex1.cpp

intn;

cin>

>

n;

endl<

f(n);

综上,得出构造一个递归方法基本步骤,即描述递归关系、确定递归边界、写出递归函数并译为代码,最后将程序完善。

以下继续引用一些例子来讨论递归方法的应用。

经典递归问题

以下讨论两个十分经典的递归问题。

它们的算法构造同样遵循刚刚得出的四个步骤。

了解这两个问题可加深对递归方法的理解。

[例2]Fibonacci数列(兔子繁殖)问题:

已知无穷数列A,满足:

A

(1)=A

(2)=1,A(N)=A(N-1)+A(N-2)(N>

=3)。

从键盘输入N,输出A(N)。

[分析]递归关系十分明显,由A(N)的表达式给出。

需要注意的是本例中对于N>

=3,A(N)的值与A(N-1)和A(N-2)都有关。

[代码]

//ex2.cpp

longfibonacci(intx)

{

if((x==1)||(x==2))

return(fibonacci(x-1)+fibonacci(x-2));

cout>

endl>

fibonacci(n);

[例3]Hanoi塔问题。

[问题描述]在霍比特人的圣庙里,有一块黄铜板,上面插着3根宝石针(分别为A号,B号和C号)。

在A号针上从下到上套着从大到小的n个圆形金片。

现要将A针上的金片全部移到C针上,且仍按照原来的顺序叠置。

移动的规则如下:

这些金片只能在3根针间移动,一次只能一片,且任何时候都不允许将较大的金片压在较小的上面。

从键盘输入n,要求给出移动的次数和方案。

[分析]由金片的个数建立递归关系。

当n=1时,只要将唯一的金片从A移到C即可。

当n>

1时,只要把较小的(n-1)片按移动规则从A移到B,再将剩下的最大的从A移到C(即中间“借助”B把金片从A移到C),再将B上的(n-1)个金片按照规则从B移到C(中间“借助”A)。

本题的特点在于不容易用数学语言写出具体的递归函数,但递归关系明显,仍可用递归方法求解。

//ex3.cpp

hanoi(intn,chart1,chart2,chart3){

if(n==1)

"

1"

t1<

"

t3<

endl;

{

hanoi(n-1,t1,t3,t2);

n<

hanoi(n-1,t2,t1,t3);

}

PleaseenterthenumberofHanoi:

;

Answer:

hanoi(n,'

A'

'

B'

C'

);

函数递归调用的应用与分治策略

许多算法都采用了分治策略求解,而可以说分治与递归是一对孪生兄弟,它们经常同时被应用于算法的设计中。

下面讨论著名的Catalan数问题,人们在对它的研究中充分应用了分治策略。

[例4]Catalan数问题。

[问题描述]一个凸多边形,通过不相交于n边形内部的对角线,剖分为若干个三角形。

求不同的剖分方案总数H(n)。

H(n)即为Catalan数。

例如,n=5时H(5)=5。

[分析]Catalan数问题有着明显的递归子问题特征。

在计算Catalan数时虽然可以推导出只关于n的一般公式,但在推导过程中却要用到递归公式。

下面讨论三种不同的解法,其中第三种解法没有使用递归,它是由前两种解法推导而出的。

[解法1]对于多边形V1V2…Vn,对角线V1Vi(i=3,4,…,n-1)将其分为两部分,一部分是i边形,另一部分是n-i+1边形。

因此,以对角线V1Vi为一个剖分方案的剖分方案数为H(i)*H(n-i+1)。

还有一种的特殊情形,是对角线V2Vn将其分为一个三角形V1V2Vn和一个n-2+1边形。

为了让它同样符合粗体字给出的公式,规定H

(2)=1。

于是得到公式:

H(n)=∑H(i)*H(n-i+1)(i=2,3,…,n-1)----公式

(1)

H

(2)=1

有了这个递归关系式,就可以用递推法或递归法解出H(n)。

[解法2]从V1向除了V2和Vn外的n-3个顶点可作n-3条对角线。

每一条对角线V1Vi把多边形剖分成两部分,剖分方案数为H(i)*H(n-i+2),由于Vi可以是V3V4…Vn-1中的任一点,且V1可换成V2,V3,…,Vn中任一点也有同样的结果。

考虑到同一条对角线在2个顶点被重复计算了一次,于是对每个由顶点和对角线确定的剖分方案都乘以1/2,故有

H(n)=n∑(1/2)H(i)*H(n-i+2)(i=3,4,…,n-1)

把(1/2)提到∑外面,

H(n)=n/(2*(n-3))∑H(i)*H(n-i+2)(i=3,4,…,n-1)----公式

(2)

规定H

(2)=H(3)=1,这是合理的。

由公式

(2)和H

(2)=1,同样可以用递推法或递归法解出H(n)。

[解法3]把公式

(1)中的自变量改为n+1,再将刚刚得出的公式

(2)代入公式

(1),得到

H(n+1)=∑H(i)*H(n-i+2)(i=2,3,…,n)由公式

(1)

H(n+1)=2*H(n)+∑H(i)*H(n-i+2)(i=3,4,…,n-1)由H

(2)=1

H(n+1)=(4n-6)/n*H(n)由公式

(2)

H(n)=(4n-10)/(n-1)*H(n-1)----公式(3)

这是一个较之前两种解法更为简单的递归公式,还可以继续简化为

H(n)=1/(n-1)*C(n-2,2n-4)----公式(4)

这就不需要再使用递归算法了。

然而在程序设计上,公式(4)反而显得更加复杂,因为要计算阶乘。

因此最后给出由公式(3)作为理论依据范例程序代码。

代码相当简单,这都归功于刚才的推导。

如果用前两种解法中的递归关系,程序会变得复杂且容易写错。

因此,有时对具体问题将递归关系公式进行必要的化简也是至关重要的。

//ex4.cpp

#defineMAXN100

longf(intx){

if(x==3)

return((4*x-10)*f(x-1)/(x-1));

\nPleaseinputNforaCatalannumber:

if((n<

=MAXN)&

&

(n>

=3))

Theansweris:

本例编程时还有一个细节问题需要注意。

注意函数f中的斜体部分,按照公式(4)计算时一定要先进行乘法再进行除法运算,因为(4*x-10)并不总能整除(x-1),如果先进行除法则除出的小数部分将自动被舍去,从而导致得到不正确的解。

数学上许多有重要意义的计数问题都可以归结为对Catalan数的研究。

可以看到,本例中的递归关系经简化还是相当简单的。

下面讨论一个递归关系略为复杂的例子。

[例5]快速排序问题。

快速排序是程序设计中经常涉及的一种排序算法。

它的最好时间复杂度为O(nlog2n),最差为O(n2),是一种不稳定的排序方法(大小相同的数在排序后可能交换位置)。

[算法描述]快速排序的一种基本思想是:

要将n个数按由小到大排列,在待排序的n个数中选取任一个数(在本例中取第一个),称为基准数,在每一次快速排序过程中设置两个指示器i和j,对基准数左边和右边的数同时从最左(i)和最右(j)开始进行扫描(i逐1递增,j逐1递减),直到找到从左边开始的某个i大于或等于基准数,从右边开始的某个j小于或等于基准数。

一旦发现这样的i和j(暂且称之为一个“逆序对”),则把第i个数和第j个数交换位置,这样它们就不再是逆序对了,紧接着再将i递增1,j递减1。

如此反复,在交换过有限个逆序对后,i和j将越来越靠近,最后“相遇”,即i和j指向同一个数,暂且称之为相遇数(极端情况下,如果一开始就不存在逆序对,i和j将直接“相遇”)。

相遇后就保证数列中没有逆序对了(除了在上述的极端情况下基准数和自身也算构成一个逆序对,注意粗体字给出的逆序对的定义)。

继续扫描,非极端情况下,由于数列中已经没有逆序对,i递增1(如果相遇数小于基准数)或者j递减1(如果相遇数大于基准数)后即算完成了一趟快速排序,这时第1到第j个数中的每个都保证小于或等于基准数,第i到第n个数中的每个保证大于或等于基准数。

此时,递归调用函数,对第1到第j个数和第i到第n个数分别再进行一趟快速排序。

如果在极端情况下,程序认为基准数和自身构成逆序对,则将基准数与自身交换(这其实没有作用)之后i递增1,j递减1(注意斜体字给出的对逆序对的处理方法),同样对第1到第j个数和第i到第n个数分别再进行一趟快速排序。

最后的问题就是确定递归边界。

由于被排序的数列将不断被划分为两个至少含一个数的子列(因为在每趟排序最后进行递归调用函数时i<

j),最后子列的长度将变为1。

这就是递归的边界。

在程序实现是,本着“能排则排”的原则,只要第一个数小于j(或者第i个数小于最后一个数),即进行递归。

[主程序(递归函数体)]

//ex5.cpp(part)

qksort(intl,intr)

inti,j,v,s;

i=l;

j=r;

v=a[l];

do

while(a[i]<

v)i++;

while(a[j]>

v)j--;

if(i<

=j)

{s=a[i];

a[i]=a[j];

a[j]=s;

i++;

j--;

}while(i<

=j);

if(l<

j)qksort(l,j);

r)qksort(i,r);

[例6]“九宫阵”智力游戏。

[问题描述]一个9×

9方阵,由9个“九宫格”组成,每个九宫格又由3×

3共9个小格子组成。

请在每个空白小格子里面填上1~9的数字,使每个数字在每个九宫格内以及在整个九宫阵中的每行、每列上均出现一次。

(1)编程将下面图中的九宫阵补充完整。

(2)讨论是否可能给出“九宫阵”的全部解?

 

9

4

5

7

3

6

8

1

2

[分析]本题可利用回溯法解决,其基本思想为深度优先搜索(DFS),这也是一种以分治策略为基础的算法。

回溯法与纯粹的DFS不同的是,它在搜索过程中不断杀死不合题意的结点。

这一点保证了解法的效率。

首先考虑如何得出全部解的情况。

解空间树容易构造,只需按顺序(从第一行第一个数字开始到第一行最后一个,然后第二行……,一直到最后一行最后一个数字)“尝试”填入数字即可。

为了解决这个问题,我们需要先编写一个函数check,其原型为intcheck(inti,intj,intk),用于求第i行第j列能否填上数字k。

如果可以,返回1,否则返回0。

由于我们是按顺序填入数字的,看起来一个数字后面的数字并不在判断能否填的范围内。

但为了解决题中某个特解问题的方便,还是引入较为严谨的判断方法。

函数check代码如下:

intcheck(inti,intj,intk){

intl,m,pi,pj;

//1.Checktheline

for(l=1;

l<

=9;

l++)

if((l!

=j)&

(a[i][l]!

=0)&

(a[i][l]==k))

return(0);

//2.Checkthecolumn

=i)&

(a[l][j]!

(a[l][j]==k))

//3.Checkthe3x3matrix

//3.1Firstlywewillhavetochecktheparent_i(pi)andparent_j(pj)

=3)pi=1;

elseif(i<

=6)pi=4;

elsepi=7;

if(j<

=3)pj=1;

elseif(j<

=6)pj=4;

elsepj=7;

//3.2Nowwecancheckit

for(l=0;

=2;

for(m=0;

m<

m++){

if(((pi+l)!

((pj+m)!

=j))

if((a[pi+l][pj+m]!

=0)&

(a[pi+l][pj+m]==k))

return(0);

结合注释很容易就能接受函数的思想,不予过多说明。

下面考虑程序最重要的部分,即递归函数。

思路是这样的:

假设某一格能填入某数,把这个格子看成解空间树的一个结点,由它可以扩展出9个儿子,即下一格填什么数(由1到9逐个尝试)。

对下一格,同样这样考虑。

不断用函数check函数考察某一个能否填入某数,一旦函数check返回0,则杀死这个结点。

如果能一直填到最后一个数,结点仍未被杀死,则这是一个解。

这种思想可用伪代码表示如下:

procedurebacktrack(i,j,k:

integer);

ifcheck(i,j,k)=truethen

begin

a[i,j]=k;

Generate_next_i_and_j;

ifi<

10then

forl:

=1to9do

backtrack(i,j,l);

end

else

Do_Output;

a[i,j]:

=0;

end;

注意斜体的“a[i,j]:

=0”必不可少!

当对某个结点(x,y)扩展的过程中,可能在扩展到(x+m,y+n)时它的子树被完全杀死(每个结点都被杀死,亦即按照(x,y)及之前的填数方案填数,无解)。

这时需要保证(x,y)以后所填的数被重新置零,这个语句的作用即在每个结点被杀死时都将其置零。

将伪代码翻译为C++代码:

backtrack(inti,intj,intk){

intl;

if(check(i,j,k)==1){

a[i][j]=k;

//Fillintheokaysolution

//Generatenexti,j

9)j++;

else{i++;

j=1;

}//EndofGeneratenexti,j

10){

backtrack(i,j,l);

output();

a[i][j]=0;

/*Whenfailsandgoesupperwards,thevaluemustbecleared*/

函数output()用双重循环完成输出。

在主函数main()对backtrack(1,1,i)进行一个循环,i从1取到9,即可完成整个程序。

运行时发现九宫格的解相当多,即使保存到文件中也不现实。

这就回答了第2个问题。

对于第1个问题,将这个程序略加改动,即赋予全局数组a以初值,并在过程backtrack中产生下一个i和j时跳过有初值的部分,即可将程序转化为求填有部分空缺的九宫格程序。

最后给出填充有部分空缺的九宫格的完整源代码。

//ex6-1.cpp

inta[11][11]={0};

output(){

inti,j;

Onesolutionis:

for(i=1;

i<

i++)

for(j=1;

j<

j++)

a[i][j]<

do{

}while(a[i][j]!

=0);

//EndofGeneratenexti,j

init(){

a[1][2]=9;

a[1][6]=4;

a[1][7]=5;

a[1][9]=7;

a[2][3]=3;

a[2][5]=7;

a[2][6]=9;

a[2][7]=4;

a[3][4]=3;

a[3][5]=6;

a[3][8]=8;

a[3][9]=9;

a[4][1]=3;

a[4][4]=1;

a[5][3]=4;

a[5][8]=2;

a[5][9]=3;

a[6][2]=1;

a[6][3]=2;

a[6][6]=3;

a[7][1]=8;

a[7][8]=5;

a[8][2]=6;

a[8][4]=2;

a[8][5]=9;

a[9][2]=2;

a[9][3]=1;

a[9][7]=8;

inti;

i++){

init();

backtrack(1,1,i);

递归方法在算法与数据结构中的应用无所不在,如动态规划(状态方程)、回溯法(深度优先搜索)等等,以上两例只是冰山一角。

只有熟悉掌握函数递归调用的编程方法,深入理解分治策略的重要思想,才能编写出功能强大、高效简明的程序。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 幼儿教育 > 幼儿读物

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1