递归调用——数学观点看递归
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
递归调⽤——数学观点看递归
想起上⼤学那会⼉递归调⽤曾是那么令⼈头痛,现在⼯作也近两年时间了,对递归倒是有了较明晰的了解.
递归,数学⾥⾯叫recursion,其实就是递推关系. 中学数学有⼀部分其实就是递归的⾮常典型的做法,不过⽼师们都没怎么扩展,新课标必修五第⼆章数列应该算是我们第⼀次接触递推的概念了.
其实说到递归,⼤伙都知道就是⾃⼰调⾃⼰,这样其实⼤家都明⽩,但是说来怎么调?如何控制?⼜如何看得到结果是想要的呢?相信还是很晕,下⾯从中学数学⾥⾯来看看吧.
第⼀部分、两个典型的例⼦,等差数列与等⽐数列
其实这实际上是⼀个例⼦,什么是等差数列?就是后⼀项总⽐前⼀项多⼀个数,这个数不变...,也有的说:“就是后⼀项减去前⼀项为⼀定常数...”
所以我们经常⽤表达式表⽰为
a(n)=a(n-1)+d\]
那⼜有问题了,这⾥确定了⼀个数列没有呢?当然没有,我说后⼀项⽐前⼀项多2,这是什么数列?
是1,3,5,7,9, ...还是2,4,6,8,10, ...
当然弄不清楚,为什么呢?因为我们谈到的数列需要有⼀个⾸项,即第⼀项的值,所以⼀旦谈到解递推数列,就应该有两个内容,⼀个是连续项间的关系,另⼀个就是⾸项关系.
那么就可以利⽤叠加的⽅法来计算了:
假设这⾥⾸项为1,也就是a(1)=1,⽽这个常数就为2
那么
a(n)=a(n-1)+2
a(n-1)=a(n-2)+2
a(n-2)=a(n-3)+2
a(n-3)=a(n-4)+2
...
a(3)=a(2)+2
a(2)=a(1)+2
将等号左边的依次相加,右边的也依次相加
这样很容易发现,左边有⼀部分从a(2)⼀直加到a(n-1),右边也有⼀部分从a(2)⼀直加到a(n-1),那么消去,就左边剩下a(n),右边就剩下a(1)和n-1个2相加了,数学公式就成了
a(n)=a(1)+2(n-1)
即
a(n)=1+2(n-1)
就是等差数列的通向公式啦,那么这个递推关系中可以看到a(n)=a(n-1)+x即为连续项间的关系,⽽a(1)=1就是⾸项啦. 那这个和递归间的关系就很明显啦
a(n)=a(n-1)+x表⽰,调⽤a(n)即执⾏a(n-1)+x,那么同时调⽤a(n-1),...这样⼀直下去,最后当a(1)时不在调⽤函数,直接返回1,再⼀路加回去,结束递归.
下⾯看看⽤代码来实现,还是假定⾸项为1,公差为2
#include <stdio.h>
int main()
{
int AddFun(int);
int num;
printf("Please input num:");
scanf("%d",&num);
printf("The result is %d",AddFun(num));
return 0;
}
int AddFun(int n)
{
if(n == 1)
{
return 1;
}
return AddFun(n-1) + 2;
}
这⾥呢AddFun(n)就是之前看到的a(n),在调⽤的过程中先判断n是否为1,如果为1当然就表⽰现在是⾸项了,不在继续调⽤,直接返回结果1
这⾥使⽤的不是数学中的公式计算,纯粹的事利⽤递推关系得到的结果.
再看看等⽐数列. 相信已经很清楚了,等⽐数列就是后⼀项是前⼀项的定常数被,表⽰为
a(n)=a(n-1)*q
LaTeX代码为
\[a_n=a_{n-1}\cdot q\]
将函数改⼀改就能使⽤了,假设⾸项为1,公⽐为2:
static void Main(string[] args)
{
Console.Write("Please input num:");
int n = int.Parse(Console.ReadLine());
int result = AddFun(n);
Console.WriteLine(result);
Console.ReadKey();
}
private static int AddFun(int n)
{
if (n == 1)
{
return1;
}
else
{
return AddFun(n - 1) * 2;
}
}
当然递归不⼀定要调⽤函数开完成,使⽤for循环⼀样可以做到
第⼆部分、递归调⽤注意(直接递归)
递归调⽤在编程⾥⾯就是对⼀个函数,或⽅法进⾏⾃⼰掉⾃⼰,那么在编写递归代码时有三点要注意,⾸先要注意⼀个问题,就是递推关系,这⾥需要抽象出⼀个递推的关系,不⼀定只有两层,或许是三层甚⾄更多
例如:楼梯有n阶台阶,上楼可以⼀步上1阶,也可以⼀步上2阶,编⼀程序计算共有多少种不同的⾛法.
先来分析⼀下,如果只有⼀个台阶,那么显然只有1种,所以a(1)=1
如果有两个台阶,呢么就有每次上⼀层,上两次,和⼀次上两层,即1+1和2共2种,即a(2)=2
当n=3时,那么就有1+1+1、1+2、2+1共3种了,似乎还看不出规律,那么分析⼀下递推关系,即前后项间的关系
到达第三层有两个⽅法,⼀是从第⼆层⾛⼀步到第三层,那么有a(2)种⽅法,⼜可以从第⼀层⾛两步到第三层,即a(1)种⽅法,拍脑袋了,显然到达第三层有a(1)+a(2)种⽅法,那么是不是呢?验证⼀下
a(1)+a(2)=1+2=3,咦,正好,看来有点像了,再来看看第四层是不是
当n=4时,按照上边的分析肯定为a(2)+a(3)=2+3=5,下⾯我们枚举⼀下:1+1+1+1、1+1+2、1+2+1、2+1+1、2+2,刚刚好,看来就是她了!
那么递推关系就有了,即a(n)=a(n-1)+a(n-2),同时还有了⾸项的值:a(1)=1,a(2)=2,由于递推关系有三项,很显然必须有两个初始值
好啦,上⾯讨论了递归的⼀个注意,下⾯是第⼆个注意,临界条件
既然递归就是⾃⼰调⾃⼰,那么怎么停⽌呢,显然不做逻辑判断,计算机会⼀直运⾏下去,知道程序崩溃(栈溢出),所以就需要加上⼀个if判断来跳出递归,其实就是前⾯提到的数列⾸项,就是这⾥的a(1)和a(2)
那么代码就可以这么写了:
private static int Count(int n)
{
if (n == 2)
{
return2;
}
else if (n == 1)
{
return1;
}
else
return Count(n - 1) + Count(n - 2);
}
实际上这就是⼀个典型的Fibonacci数列,只是初始值不同罢了。
现在剩下第三个要注意的事项了,就是递归体.
先看两个函数代码(C++代码):
void Test_1(int n)
{
cout<<n<<endl;
if(n < 6)
{
Test_1( n + 1 );
}
}
2、
void Test_2(int n)
{
if(n < 6)
{
Test_2( n + 1 );
}
cout<<n<<endl;
}
如果在main()函数中分别调⽤这两个函数,会有什么执⾏结果呢?
现在我们假定n为0
第⼀个函数打印出
1
2
3
4
5
6
第⼆个函数打印出
6
5
4
3
2
1
这个就很奇怪了,这是为什么呢?
实际上前⾯我们考虑的递归关系是单⼀的递归,但是这两个递归体中,除了完成递归外,还有其他执⾏代码.
其实之前也有,只是没有显⽰出来,就没有被察觉⽽已
那么怎么去分析呢?
其实很简单,递归体本⾝可分为三个部分,⼀个是头代码,⼀个是递归表达式,⼀个是尾代码,区分头尾的⾃然就是递归表达式了看这段代码:
#include <iostream>
using namespace std;
int main()
{
void Test(int);
Test(1);
return0;
}
void Test(int n)
{
cout<<n<<endl;
if(n < 3)
{
Test( n + 1 );
cout<<n<<endl;
}
}
输出结果为
1
2
3
2
1
那么在这段代码中if()前⾯的输出流为头代码,中间的Test( n + 1 )为递归表达式,这⾥的尾代码依然是⼀个输出流
那么分析此程序怎么做呢?很简单,⼋个字
“头尾分开,等待归来”
头尾分开,就是将两个cout<<n<<endl分开,第⼀个cout<<n<<endl执⾏,然后转⼊第⼀次递归,等待递归结束后回到第⼆个cout<<n<<endl 语句执⾏
这样说很抽象,我们⼀步步分析⼀下
第⼀次调⽤,n=1
执⾏头cout<<n<<endl,输出1,并且尾cout<<n<<endl等待,其值也是1,但未输出;
进⼊第⼀次递归,n=2
执⾏头cout<<n<<endl,输出2,并且尾cout<<n<<endl等待,其值是2,但未输出;
进⼊第⼆次递归,n=3
执⾏头cout<<n<<endl,输出3,但判断n < 3 不成⽴,即不再执⾏尾cout<<n<<endl语句,函数体结束
回到第⼀次递归体,执⾏等待的尾cout<<n<<endl,输出2,第⼀次递归体结束,会带最外层
执⾏等待的cout<<n<<endl,输出1,整个函数调⽤结束,回到main()函数体继续执⾏其他命令.
形象的说就像是⼀架⼿风琴⼀样,函数调⽤开始就拉开⼿风琴,然后进⼊递归,等待内层递归结束才执⾏后⾯的代码
也就⼋个字“头尾分开,等待归来”
其实这就是实现的栈的结构
到这⾥就基本结束了我所理解的递归含义,总结⼀下:
递归调⽤即递推关系
递归三个注意:注意递推表达、注意临界判断、注意递归体。