算法总结之递推与递归
递推算法
递归算法大致包括两方面的内容:1)递归起点 ; 2)递归关系
递推起点
递归起点一般由题目或者实际情况确定,不由递归关系推出。如果无法确定递归起点,那么递归算法就无法实现。可见,递归起点是递归算法中的重要一笔。
递推关系
递归关系是递归算法的核心。常见的递归关系有以下几项:
- 1)一阶递推;
- 2)多阶递推;
- 3)间接递推;
- 4)逆向递推;
- 5)多维递推。
下面通过栗子来详细介绍一下上述类别的递推关系。
1. 一阶递推
在计算f(i)时,只用到前面项中的一项,如等差数列。公差为3的等差数列,其递推关系为:
f(i)=f(i-1)+3
eg. 平面上10条直线最多能把平面分成几部分?
分析:以直线数目为递推变量,假定i条直线把平面最多分成f(i)部分,则f(i-1)表示i-1条直线把平面分成的最多部分。在i-1条直线的平面上增加直线i,易得i与平面上已经存在了的i-1条直线最多各有一个交点,即直线i最多被分成i段,而这i段将会依次将平面一分为二,即直线i将最多使平面多增加i部分。所以,递推关系可表示为:f(i)=f(i-1)+i
易得当0条直线时,平面为1部分。所以f(0)=1为递推起点。
上述分析可用下面代码表示(c++):
#define MAX 100
int f[MAX] //存放f(i)
int lines(int n){
//输入n为直线数目
//输出最多部分数
int i;
f(0)=1;
for(i=1;i<=n;i++){
f[i]=f[i-1]+3;
}
return f[i];
}
2. 多阶递推
在计算f(i)时,要用到前面计算过的多项,如Fibonacci数列。
eg.求Fibonacci的第10项。
分析:总所周知,Fibonacci数列中的第n项等于第n-1项加上n-2项。所以递推关系为
f(i)=f(i-1)+f(i-2);且f[0]=f[1]=1。
C++代码如下:
#define MAX 100
int f[MAX];
int fib(int n){
//输入n为项数
//输出第n个fib数
int i;
f[0]=0;
f[1]=1;
for(i=2;i<=n;i++){
f[i]=f[i-1]+f[i-2];
}
return f[n]
}
3. 间接递推
在计算f[i]时需要中间量,而计算中间量要用到之前计算过的项。
eg.现有四个人做传球游戏,要求接球后马上传给别人。由甲先传,并作为第一次传球。求经过10次传球,球仍回到发球人甲手中的传球方式的种数。
分析:定义两个状态,1)当前球在甲上,经过i次传球之后球仍在甲上,此状况记为F,其传球方式的种数为f(i);2)当前球不在甲手上,经过i次传球之后球在甲手上,此状态记为G,其传球方式的种数为g(i)。
对于状态1):甲传出一个球之后,接球的人的状态便变成G(i-1)了,由于甲可以传给3个不同的人,所以f(i)=3g(i-1);
对于状态2):持球者可以选择把球传给甲,此时是F(i-1)状态;也可以把球传给另外两个人,即2G(i-1)状态。所以g(i)=f(i-1)+2*g(i-1).
计算递推起点,由于甲第一次不可能把球传给自己,所以f(1)=0;其他人要传一次球就把球传给甲,那只有一种方式(直接把球传给甲),即g(1)=1.
上述递推关系便是间接递推。用c++实现如下:
#define MAX 100
int f[MAX];
int g[MAX];
int ball(int n){
//输入n为传球次数
//输出为传球方式的种数
int i;
f[1]=0;
g[1]=0;
for(i=2;i<=n;i++){
f[i]=3*g[i-1];
g[i]=f[i-1]+2*g[i-1];
}
return f[n];
}
4. 逆向递推
顾名思义,就是从后面开始往前推。
eg.硬币下棋游戏。棋盘上标有第0站,第1站...第100站,一开始棋子在第0站,棋手每次投一次硬币,若硬币正面向上,则往前跳两站;否则,往前跳一站...直到棋子跳到第99站(胜利大本营),第100站(失败大本营)时,游戏结束。如果硬币出现正反面的概率均为0.5,分别求出棋子到达胜利大本营和失败大本营的概率。
分析:假设记从第i站开始,最后到达100站的概率为f(i)。而从第i站,投掷一次硬币,有0.5的概率到达第i+1站,有0.5的概率到达i+2站。所以递推关系为:f(i)=0.5f(i+1)+0.5f(i+2).
易得递推起点f(100)=1,f(99)=0.因为到达99站,游戏结束。
上述就是逆递推的一个过程。c++实现如下:
#define MAX 100
double f[MAX];
double prob(){
//无输入
//输出为到达100站的概率
int i;
f[100]=1.0;
f[99]=0;
for(i=98;i.=0;i--){
f[i]=0.5*f[i+1]+0.5*f[i+2];
}
return f[0];
}
5. 多维递推
元素处于一个多维矩阵中,递推需要使用矩阵中其他位置的元素。
例子日后更新。
递归函数
在计算机科学中,如果一个函数的实现中,出现对函数自身的调用语句,则该函数称为递归函数。
递推算法可以用递归函数来实现。一般来说循环递推算法比递归函数要快,但递归函数的可读性更棒。
把上面的部分递推算法改写成递归函数。
1)平面划分
int lines(int i){
if(i<=0)
return 1;
else
return lines(i-1)+i;
}
2)Fibonacci数
int fib(int i){
if(i==0)
return 0;
if(i==1)
return 1;
else
return fib(i-1)+fib(i-2);
}
由上面的代码可以分析到,递推起点在递归函数中起到了递归截止作用。
递归函数的执行过程
递归函数每次调用自身都会生成一个激活帧(包含程序的参数、局部变量、返回值、以及该程序执行完毕后返回上一层的指令地址等),同时把计算控制交给下一次调用。这些激活帧存在在系统中先进后出的栈里。所以,程序的递归调用过大的话,会引发栈溢出。
尾递归
在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。
上面说到递归函数需要在调用多次时需要保留很多激活帧,这会引发栈溢出。但如果采用尾递归的话,就可以避免这个情况。因为尾递归在程序的最后动作只是调用函数,不涉及其他计算问题,所以可以优化删去很多中间的激活帧。
如上面递归函数fib(),其最后一步就涉及加法,所以不是尾递归,但可以把它改成尾递归。如下:
int fib(int n,int f1,int f2)
{
//初始f1=0;f2=1
if(n==0)
return f1;
else
return fib(n-1,f2,f1+f2)
}
由上面代码可看到,函数的最后调用就是一个函数,不涉及其他计算。
小结
递归函数一定要有递归起点作为递归结束标志。