算法总结之递推与递归

递推算法

递归算法大致包括两方面的内容: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)状态;也可以把球传给另外两个人,即2
G(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)
	}

由上面代码可看到,函数的最后调用就是一个函数,不涉及其他计算。

小结

递归函数一定要有递归起点作为递归结束标志。

posted @ 2018-10-02 16:21  Sure_Cheun  阅读(5005)  评论(0编辑  收藏  举报