递归的定义
当定义一个过程(往往是函数)时出现调用本过程(函数)的成分称为递归。拿紫书的一个例子来说:
递归:参见“递归”。
递归:如果还是没有理解,参加“递归”。
当然这两个递归都是一个死循环,实际使用时需要避免这样的递归。这种直接调用自身的递归,称为直接递归。如果过程或函数p调用过程或函数q,而q又调用p的递归称为间接递归。任何间接递归算法都可以转换为直接递归。
如果一个递归过程或递归函数中的递归调用语句是最后一条执行语句,则称这种递归调用为尾递归。
直接看下面这个比较简单的例子。
例 5.1
设计一个递归算法来求n!。
分析:这个大家应该都会,就直接看代码了:
int fun(int n){
if (n==1) return(1); else return(fun(n-1)*n);}
其中递归调用是最后一句语句,所以它属于尾递归。但是大家可以发现,这个递归算法就复杂度而言和for循环几乎是一样的。
事实上递归算法通常是将一个大的复杂的问题转化为一个或多个与原问题相似的规模较小的问题来求解。因为只需少量地代码就可以描述出解体过程中所需要地多次重复计算,所以大大地减少了算法的代码量。
但是递归算法并不改变原非递归算法的本质,所以并不会在时间复杂度上做到优化。
总结上面的话来说,递归算法的优点是结构简单,清晰,易于阅读,缺点就是效率低且不易于优化。
何时使用递归
一般在以下三种情况使用递归:
1.定义是递归的
比如有些数学公式,数列的定义只给出它的递归公式(Fibonacci数列),对于这些问题的求解可以直接用递归进行运算(可以推出直接的公式,但在计算机中的效率往往不会比递归算法更高)。
2.数据结构是递归的
链表二叉树很多都是啦。
3.问题的求解方式是递归的
额这个情况非常多,书上提到的是比较常见的hanoi塔问题,事实上还有履带染色问题,动态规划等一系列采用数学归纳法求解的问题。
Hanoi 问题
有3个X,Y,Z三个塔座,在塔座X上有n个直径互不相同的盘片,从小到大依次编号为1~n。现要求将这n个盘片移到塔座Z上且保持相同的顺序,但必须遵循以下两个规则:
1.每次只能移动一个盘片到其他塔座。
2.不能将一个编号较大的盘片移动到编号较小的盘片之上。
设计求解该问题。
分析:非常非常经典的问题,我们记将n个盘片按照规则从一个塔座X放到塔座Z且塔座Y为空的最优方案为H(n,X,Y,Z)。
n=1时,就放一下就行了。
如果n=i的方案已确定(i>=1),n=i+1可以先将X塔座的i个盘片按照H(i,X,Z,Y)的方案放到Y塔座上,再将X塔座最下面的那个盘片放到Z塔座上,最后将Y上的i个盘片按照H(i,Y,X,Z)的方案放到Z塔座上。
可以很明显的发现,n=i+1的最优方案需要调用n=i的最优方案得到,即满足递归调用的特性,由此得到的递归算法如下:
void H(int n,char X,char Y,char Z){
//结束递归的条件
if (n==1) printf("\t将第%d个盘片从%c移动到%c\n",n,X,Z);
else{
//递归调用的主体
H(n-1,X,Z,Y);//将X塔座的i个盘片按照H(i,X,Z,Y)的方案放到Y塔座上
printf("\t将第%d个盘片从%c移动到%c\n",n,X,Z);//再将X塔座最下面的那个盘片放到Z塔座上
H(n-1,Y,X,Z);//最后将Y上的i个盘片按照H(i,Y,X,Z)的方案放到Z塔座上
}
}
栈和递归
不用书上那么书面的语言,就是说:函数调用的双向数据传递需要通过函数参数和返回值来实现,还需要在进入函数时为函数的局部变量分配存储空间,在退出函数时回收(销毁)这部分空间(这就是为什么对函数内的非指针变量进行改变不会影响到实参)。
于是大多数CPU上的程序用栈来实现函数调用的操作,这个函数调用栈使用的结构叫做栈帧结构。每调用一个函数都会相应地创建一帧压入到调用栈中,这个栈包括函数返回地址,函数实参和局部变量3个属性。
函数一旦执行完毕,对应的帧出栈,控制权交还给上层调用函数,即当前栈顶帧对应的函数。
如果是直接递归调用,每次调用自身的代码,但是每次返回的地址是不同的,相当于每一次递归调用时对自身函数进行了一次复制,调用自身函数的复制件,因而保证各个部分的独立性(相当于,但不是复制,而是采用代码共享的方式)。
递归到非递归的转换
上面介绍了这么多,主要就是可以用栈来模拟递归算法(例如栈那一章的迷宫问题),下面以Hanoi问题为例。
同样我们将n=k,从X塔座移动到Z塔座,Y塔座为空的方案记作H(k,X,Y,Z)。不过需要注意的是,上面的定义是函数,这里定义的是一个结构体。
我们以这个结构体定义一个栈,将H(n,X,Y,Z)即需要解决的问题压入栈,但是对于这个问题我们显然无法直接解决,于是和上面一样将这个问题出栈,然后分裂成两个子问题即H(n-1,X,Z,Y)和H(n-1,Y,X,Z),将这两个问题(结构体)入栈,当然中间需要打印移动一次盘片的操作。
接下来就是重复操作不断地出栈进栈,将复杂的问题出栈分裂成小的子问题进栈,小问题当n=1时不再进行分裂打印后直接出栈,直到栈为空运行完毕。
其实大家仔细想一想,这里的问题(结构体)即是函数,栈即是函数调用栈和递归算法是一样的。