递归的优化方法

  递归的优化包括时间复杂度上的优化以及空间复杂度上的优化两种。
  如果递归能在空间上做到优化,不但能节省栈的空间,同时节省了调用函数的时间,也能大大加快程序的运行速度。

时间复杂度的优化

是否重复计算

在递归的过程,很多时候一些子问题是被重复计算的

斐波那契数列

int Fibonacci(int n)
{
   if (n <= 1) return n;
   return Fibonacci(n - 2) + Fibonacci(n - 1);
}

在这里插入图片描述
由上图可见,fib(4), fib(3), fib(2), fib(1)均被重复计算,此时时间复杂度为 O ( 2 n ) O(2^n) O(2n)

优化方法
我们可以将计算的结果保存起来,就可以避免重复进行计算。
可以用数组或者是哈希表进行存储,通过查表判断是否有重新计算过,如果有则无需再算。

int Fmap[1000];		// 这里用数组,将Fmap全部初始化为-1(代表未计算过)

int Fibonacci(int n)
{
   if (n <= 1) return n;

   if(Fmap[n] != -1)		// 不等于-1 说明计算过
       return Fmap[n];

   Fmap[n] = Fibonacci(n - 2) + Fibonacci(n - 1);
   return Fmap[n];
}

通过这样优化,只需要Fib(n), Fib(n-1), …, Fib(1)每个计算一次,因此时间复杂度为 O ( n ) O(n) O(n)
PS: 如果有学习过动态规划的同学,会发现这种优化本质其实是动态规划的思想。

空间复杂度的优化

1 尾递归

尾递归,顾名思义,就是在函数体的尾部再进行递归(调用自己)。

void func1(int n)	// 非尾递归
{
   int i = 1;
   return i + func1(n-1)		// 先求func(n-1) 再计算加法 再返回
}

void func2(int n, ...)	// 尾递归 省略号是指一般尾递归会有另外参数作为暂存变量
{
   int i = 1;
   return func2(n-1, ...)		// 直接将func(n-1, ...)返回
}

通过上面两个例子,可以大致理解什么才叫作尾递归。
在func1中,先计算func(n-1),然而func(n)函数并没有结束,还要计算一个加法,所以并不是在函数的尾部才调用自己。
在func2中,在return直接返回递归函数的下一层,才能称作尾递归。

斐波那契数列的尾递归写法

int Fibonacci(int n, int x, int total)
{
   if (n <= 1) return x;

   return Fibonacci(n - 1, total, x + total);	// 实际上是一种迭代
}

int main()
{
   cout << Fibonacci(3, 1, 1);    // 斐波那契数列第三项
}

优化原理
非尾递归的情况,因为递归函数还需要返回,继续执行下面的部分代码(如上面例子的加法),所以每次进入下一层递归,会对当前层的局部变量进行入栈保存,如果递归层数太多,则会溢出。

尾递归的情况,因为函数在最后才进行递归,后面已经没有需要再计算的了,所以递归前面的局部变量无需进行保存。因此,其实尾递归只需用到一层栈,每到下一层递归,当前层函数就退栈,下一层函数就入栈。

然而并不是所有语言的编译器都能对尾递归进行优化,也就是说,就算你写成了尾递归的形式,有些编译器还是会随着递归不断入栈(而不是只用一层栈),因此便是需要手动写代码进行尾递归优化。

手动进行尾递归优化
  尾递归,其实可以看做一种特殊的迭代,因此如果递归可以改写成尾递归(不是所有递归都可以),那么就可以通过写成迭代的方式,手动实现尾递归的优化。
  换句话说,尾递归是迭代的一种特殊写法,二者时间和空间复杂度是相同的。尾递归并没有对时间复杂度进行任何优化,单纯的只是将空间复杂度缩小为O(1)。

2 在函数体内多次递归

如果递归函数只是调用自己一次,相应的递归树是一棵斜树。如果调用自己两次,则是一棵二叉树。如果调用自己多次,则树的结点有多个分支。如果随着树的结点分支增多,一些子树的深度减小,则总体的递归树深度会减少,出现递归过深导致溢出的可能性就降低。

示例 快速排序

// 传统 未优化
void Qsort1(vector<int> &vec, int low, int high)
{
   int pivot;
   if(low < high)     
   {
       pivot = Partition(vec, low, high);    
       Qsort1(vec, low, pivot - 1);     // 对支点左边序列 快速排序
       Qsort1(vec, pivot + 1, high);    // 对支点右边序列 快速排序
   }
}

// 对递归进行了优化
void Qsort2(vector<int> &vec, int low, int high)
{
   int pivot;
   if(low < high)
   {
       // 采用迭代方式 缩小栈的深度
       while(low < high)
       {
           pivot = Partition(vec, low, high); 	// 分成左右两个“一小一大”序列
           Qsort2(vec, low, pivot - 1);   // 只对左半边进行快速排序
           low = pivot + 1;
       }
   }    
}

这是对传统的快速排序中递归的部分的一种优化方式。Qsort1是传统的快速排序,Qsort2是进行递归优化的快速排序。

Qsort2的原理:首先将序列分两半,取出左半边,进行快速排序。右半边先不排序,再次切两半,对右半边的左半边进行快速排序,右半边的右半边继续切两半,…如此迭代

可以得到Qsort2的递归方程
T ( n ) = T ( n 2 ) + T ( n 4 ) + T ( n 8 ) + . . . + T ( 1 ) = ∑ i = 1 l o g n T ( n 2 i ) T(n) =T(\frac{n}{2}) +T(\frac{n}{4})+T(\frac{n}{8})+...+T(1)=\sum\nolimits_{i=1}^{logn} T(\frac{n}{2^i}) T(n)=T(2n)+T(4n)+T(8n)+...+T(1)=i=1lognT(2in)
所以递归树的结点有logn个分支,且每个分支的深度不断减少。
最左边的分支 T ( n 2 ) T(\frac{n}{2}) T(2n),有logn层,最右边的分支只有一层。

因此,传统的Qsort1的递归树是logn层的满二叉树,现在优化后的Qsort2的递归树是logn层的不对称的树(左边多,右边少)。
总结:两个算法使用的总的空间应该是一样的(树的结点数相同),但是Qsort2只有最左边的分支达到了logn层,其他的分支的层数很小,因此很快就能释放,所以Qsort2不容易溢出。

ps:很多地方说快速排序的这种写法是尾递归优化,然而根据定义这显然是有误的。

综合应用

示例 x的n次方
通过递归的方式计算x的n次方

int xpow(int x, int n)
{
   if (n == 0) return 1;
   return xpow(x, n - 1) * x;
}

这个算法的时间复杂度为 O ( n ) O(n) O(n),有没有办法将它优化到 O ( l o g n ) O(logn) O(logn)?

首先这个算法的递归树是斜树,可以想办法将结构变为二叉树。

int xpow(int x, int n)
{
   if (n == 0) return 1;
   
   if(n % 2 == 1)
   	return xpow(x, n/2) * xpow(x, n/2) * x
   else
   	return xpow(x, n/2) * xpow(x, n/2);
}

考虑是否有重复计算的部分(是否能优化树的部分分支)
可以很明显看出 xpow(x, n/2) 重复计算

int xpow(int x, int n)
{
   if (n == 0) return 1;
   
   int t = xpow(x, n/2);
   if(n % 2 == 1)
   	return t * t * x
   else
   	return t * t;
}

优化后该算法的时间复杂度为 O ( l o g n ) O(logn) O(logn)

总结一下,虽然上述例子稍微有点特殊,但是思路是可以借鉴的。一连串的递归首先考虑是否能转化成树的结构,然后再观察树的一些分支是否重复计算来进行优化。

  • 3
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值