函数递归调用是很常见的做法,但是它往往是低效的,本文探讨优化递归效率的思路。
1.尾递归转换成迭代
尾递归是一种简单的递归,它可以用迭代来代替 比如 求阶乘函数的递归表达
{
if(n<=0)return 1;
return n*f(n-1);
}
可以转换成完全等价的循环迭代
{
int r=0;
while(n-->0)
r*=n;
return r;
}
尾递归是最简单的情形,好的编译器甚至可以自动的识别尾递归并把它转换成循环迭代。
2.动态规划
我一直把动态规划看作尾递归的推广(个人观点),在2维或更高的情况下,直接使用递归会造成大量的重复计算,例如在斐波那契数列的递归关系 Fib(n+2)=Fib(n+1)+Fib(n)中 Fib(3)在计算Fib(4)和Fib(5)都会用到 他被重复计算了2遍,当数列长度增大时 这种浪费会变得越来越明显。
动态规划方法将可能需要的结果全部计算出来 并保存 一般来说 动态规划从最小数开始 自底向上计算所有值(因为后面的结果会用到前面的结果) 直到得到的结果中包含了要求的结果。
{
if(n==1)return 1;
if(n==0)return 0;
return Fib(n-1)+Fib(n-2);
}
int Fib(unsigned n)
{
int* f=new int[n+1];
f[1]=1;f[0]=0;
for(int i=0;i<=n;i++);
f[i]=f[i-1]+f[i-2];
int r=f[n];
delete[] f;
return r;
}
动态规划不会造成重复运算 但是 它可能计算不需要的结果 例如关系式
a(n)=a(n-2)+a(n-4);
使用动态规划会计算很多不需要的结果,尽管如此,它的效率远远高于直接递归运算。
实际上,大部分时候动态规划把指数级时间复杂度的递归运算变成了多项式级时间复杂度的递推。
3.备忘录
减少重复值的另一个方法是使用备忘录,每次成功计算一个结果 ,就将它存入备忘录中,当再次遇到此问题时,无需重复计算,直接取出即可。
备忘录方法和直接递归相似,只是在函数在计算之前先访问备忘录,如果在备忘录中找到,就无须再计算,直接返回。
备忘录结构可以使用关联数组 哈希表等实现,特别地,当递归参数是整数时 直接用数组就可以了。
与动态规划相比,备忘录消耗了额外的备忘录查找时间,并且和直接递归一样 有大量的多余函数调用开销,但它不会造成额外计算。
4.内联
这是来自Efficient C++的方法,C++编译器不会把递归函数内联,这样,函数调用的开销变得很大。为了提高效率,必须手动内联函数。
递归函数无法完全内联,但是我们可以把它展开,这是前面Fibnacci函数的一层展开
{
if(n==1)return 1;
if(n==0)return 0;
int fn1,fn2;
if(n-1==1)fn1=1;
else if(n-1==0)fn1=0;
else fn1=Fib(n-2)+Fib(n-3);
if(n-2==1)fn2=1;
else if(n-2==0)fn2=0;
else fn2=Fib(n-3)+Fib(n-4);
return fn1+fn2
}
5.解递归
尽管我们倾向于虐待计算机 让它帮我们处理较复杂的问题,但是很多时候我们需要获得效率,就必须自己动手,其实很多递归式可以手动解出,组合数学为我们提供了不少工具:
(1)解齐次递归方程
简单的递归方程可以按以下步骤求解: