最近在实际问题中遇到了好几个需要使用递归函数才能解决的问题。在这里做个总结。所谓递归函数:就是直接或者间接的调用自身的函数。一种通俗的说法便是:递归就是用自己的简单情况定义自己。
先举一个关于递归应用的简单例子:
楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶,问共有多少种不同的走法.
该问题的解法可以典型的用到递推公式:
由于n=1时f(1)=1,f(n)数列是一个fibonacci数列,故用一般的递归程序就可得出结果来了。
对于较复杂的递归程序,《c和指针》上说“一旦理解了递归,阅读递归函数最容易的方法不是纠缠于它的执行过程,而是相信递归函数会顺利的完成它的任务。如果你的每个步骤正确无误,你的限制条件设置正确,并且每次调用之后更接近于限制条件,递归函数总是能够正确的完成任务”。比较赞同这一点,个人觉得使用递归的熟练程度体现了一个人的自信程度。而阅读递归程序时相信递归会顺利的完成任务而不要过于纠缠其细节也是人自信的体现。
最典型的使用递归方法的是全排列的实现。
下面这个全排列分析及实现均来自:http://blog.chinaunix.net/u2/81947/showart_1330222.html
全排列分析
通常我们希望检查n个不同元素的所有排列方式来确定一个最佳的排序。比如a,b,c的排列方式有abc,acb,bac,bca,cab,cba六种。
由于采用非递归的c++函数来输出排列方式很困难,所以采用递归是一种比较不错的方法。
其核心是:将每个元素放到n个元素组成的队列最前方,然后对剩余元素进行全排列,依次递归下去
比如:a b c
首先将a放到最前方(跟第一个元素交换),然后排列b c,然后将a放回本来位置
结果 a b c; a c b
其次将b放到最前方(跟第一个元素交换),然后排列a c,然后将b放回本来位置
结果 b a c; b c a
。。。
如果是4个元素,就将元素依次放到第一个元素的位置,后面的排序类似前面的3元素排序。
算法实现
/*
* 函数模板,递归调用对一个数组进行全排列
*/
template <class T>
void Perm(T list[],int k,int m)
{
int i;
if(k == m)//输出一个全排列
{
for(i=0;i <= m;i++)
cout<<list[i];
cout<<endl;
}
else //list[k:m]有多个排列方式
//递归的产生这些排列方式
for(i = k;i <= m;i++)
{
Swap(list[k],list[i]);//交换位置,逐步前提
Perm(list,k+1,m);
Swap(list[k],list[i]);//将位置还回去,对下一次排列负责
}
}
template<class T>
inline void Swap(T& a,T& b)
{//exchange a and b
T temp = a;a = b;b = temp;
}
//一个测试程序
int main()
{
char list[]="abc";
Perm(list,0,4);
return 0;
}
递归解决问题和非递归解决问题的一个对比
对任意一个数,求它的除1外的奇数约数。比如,135,它的全部奇数约数(1除外)为:3,5,9,15,27,45,135。
我编了一个程序,思路是这样的:
1.首先求出它的全部奇数素数因子,比如135的全部奇数素数因子为3,3,3,5
2.用这些因子两两,三三,四四……相乘得出奇数约数
第一步的思路比较清楚,第二步算的时候写循环比较繁复,并且输出为:
3 3 3 5 9 9 15 9 15 15 27 45 135有重复数
我的非递归版本:
int main()
{
Factorization(135);
return 0;
}
csdn成员mstlq的递归版本(其中注释是我现在加进去的):
原程序在:http://topic.csdn.net/u/20100131/23/1bb2d017-4d0a-4634-a28b-00ad3dad81f2.html
void init()
{
memset(priDivs,0,32*sizeof(unsigned int));
memset(divCnts,0,32*sizeof(unsigned int));
divC=0;
};
void getPriDivs(unsigned int n)
{
--divC;
while(n%2==0) n/=2;
for(unsigned int i=3; i<=n; i+=2) {
if(n%i==0) ++divC;
while(n%i==0) {
priDivs[divC]=i;
++divCnts[divC];
n/=i;
}
}
++divC;
return ;
};
void showOddDivs(unsigned int pro,unsigned int dep)
{
if(dep==divC)//如果递归深度达到奇数素数的个数,如果预乘子为1,则输出提示;否则输出结果
(pro==1)? (cout<<"所有奇约数是:/n") : (cout<<pro<<'/t');
else
for(unsigned int i=0;i<=divCnts[dep];++i,pro*=priDivs[dep])
showOddDivs(pro,dep+1);//依次求除第一个奇数素数外后面部分的奇数的乘积;
//第一个奇数素数的一重积与后面部分的积;..第一个因素的divCnts[dep]重积与后面部分的奇数的乘积
return;
}
void Factorization(unsigned int n)
{
init();
getPriDivs(n);
showOddDivs(1,0);
return ;
}
int main()
{
Factorization(1000000000);
return 0;
}
对比这两个程序可知:第二个比第一个精短很多,效率上也更快;逻辑上也更容易理解。
可见,要是善于使用递归,威力无穷。但也不是绝对的,递归的程序实现起来不如递归的思想那般简洁。思想上,递归简洁易懂;实现上,对计算机的时空开销,递归有时大得惊人:如果上面那个求n阶台阶的行走方法数采用递归,在计算f(10)时,f(3)的值被计算21次,在计算f(30)时,f(3)的值要被计算317877次(这个分析可由fibonacci数列推出),并且递归函数执行过程中所声明的变量是创建于运行时堆栈,并且直到该函数结束前变量一直存在,在时间和空间上,这个额外的开销相当恐怖。
一般像上面这种递归调用是递归函数所执行的最后一项任务时,我们称这种递归为尾部递归(tail recursion),可以把尾部递归函数很容易的改写成一个简单的循环,完成相同的任务,以避免运用递归可能带来的的大的时空开销。
long f(int n)
{
long fn = 1;
long f1 = 2;
long f2;
while(n > 2)
{
n--;
f2 = f1;
f1 = fn;
fn = f2 + f1;
}
return fn;
}