设为首页 加入收藏

TOP

2.7 递归和效率
2014-03-11 13:06:10 来源: 作者: 【 】 浏览:183
Tags:2.7 效率

2.7  递归和效率

递归是强大的问题求解技术,常用来为最复杂的问题生成清晰的解决方案。与迭代解决方案相比,该方案易于理解和描述。通过使用递归,可以简洁明快地实现解决方案。

本章列举了大量示例,以帮助您进一步理解递归,从而能构建自己的递归解决方案。大多数例子都很简单。但遗憾的是,本章介绍的许多递归解决方案效率不高,并不实用。不过,递归函数binarySearch和solveTowers是例外,它们的效率很高2。

有些递归解决方案效率不高,原因有以下两点:

与函数调用相关的开销

一些递归算法本来就效率不高

注释:导致某些递归算法效率不高的原因

第一个因素不是递归函数特有的,而是普遍存在于各类函数。在C++和其他高级编程语言的大多数实现中,函数调用会导致开销增大。如前所述,每次函数调用都会生成一个活动记录,记录类似于箱式跟踪中的箱。递归函数扩大了这种开销,因为对函数的一次初始调用可生成大量递归调用。例如,调用factorial(n)生成n次递归调用。另一方面,由于递归调用的模块化,因此能够明确地阐明复杂程序,代码清晰带来的补偿往往超过额外开销导致的损失。

注释:递归可以将复杂的解决方案简单化

但不能滥用递归。例如在实际中,不应使用递归函数计算阶乘。有了本章开始给出的迭代定义,可编写出迭代阶乘函数。迭代函数几乎与递归函数同样清晰,但效率更高。若递归没有优势,则不必为使用递归而付出开销代价。总而言之,在简单迭代解决方案无法求解的情况下,递归才真正显出价值。

注释:如果递归解决方案效率不高并且存在清晰高效的迭代解决方案,则不要使用递归

有关递归和效率的第二点是一些递归算法的内在低效性。低效问题与上述问题大相径庭。它与编译器如何实现递归函数没有任何关系,而与算法使用的求解函数相关。

例如,回顾本章前面的兔子繁殖问题的递归解决方案。
 

注释:兔子繁殖问题的递归解决方案本身效率就不高

图2-19演示了rabbit(7)的计算。前面曾提过一个问题:若计算rabbit(10),递归图会怎样?分析这个问题可知:图将占据本章大多数空间。rabbit(100)又将如何?它将充满整个世界。

对于兔子繁殖问题,根本问题在于它反复计算同一个值。例如,由rabbit(7)的图可知,计算rabbit(3)达5次之多。若n变大,则许多值将重复计算近万亿次。由于计算次数过多,该解决方案不可行。即使每次计算仅需很少时间,总时间还是相当惊人。

递归关系并非一无是处。要解决兔子繁殖问题,一种方法是根据同一个递归关系构建迭代解决方案。迭代解决方案向前行,不后退,而且每个值只计算一次。可以用以下迭代函数来计算rabbit(n),即使n值非常大也没有问题。
 

  1. /** Iterative solution to the rabbit problem. */  
  2. int iterativeRabbit(int n)  
  3. {  
  4. // Initialize base cases:  
  5. int previous = 1;               // Initially rabbit(1)  
  6. int current = 1;                    // Initially rabbit(2)  
  7. int next = 1;                           // Result when n is 1 or 2  
  8. // Compute next rabbit values when n >= 3  
  9. for (int i = 3; i <= n; i++)  
  10. {  
  11. // current is rabbit(i - 1), previous is rabbit(i - 2)  
  12. next = current + previous;          // rabbit(i)  
  13. previous = current;                                 // Get ready for next iteration  
  14. current = next;  
  15. } // end for  
  16. return next;  
  17. }               // end iterativeRabbit 

注释:可以使用兔子繁殖问题的递归关系创建效率更高的迭代解决方案

所以,迭代解决方案比递归解决方案效率高。在有些情况下,更容易找出递归解决方案。因此,有时须将递归解决方案转换为迭代解决方案。若递归函数调用自身一次(而非多次),则转换过程更容易。在确定函数是不是多次调用自身时,要认真分析。函数rabbit调用本身两次;但对于binarySearch函数,尽管C++代码中有两次调用,而实际上,该函数只调用自身一次。这两个调用出现在if语句中,只有其中之一会执行。

注释:如果递归解决方案容易发现但是迭代解决方案效率更高,则将递归转换为迭代

如果函数的最后一个操作是单个递归调用,那么将递归解决方案转换到迭代解决方案会更容易。这种情况称为尾递归(tail recursion)。例如,writeBackward函数使用一个尾递归,因为该函数的最后一个操作是递归调用。不要认为这是显而易见的,请看函数fact。虽然其递归调用出现在函数定义的最后,但fact的最后操作是乘。所以说,fact不是尾递归。

注释:尾递归函数

回顾writeBackward的定义:
 

  1. void writeBackward(string s)  
  2. {  
  3. int length = s.size();  
  4. if (length > 0)  
  5. {  
  6. // Write last character  
  7. cout << s.substr(length - 1, 1);  
  8. writeBackward(s.substr(0, length - 1)); // Write rest  
  9. }   // end if  
  10. }  // end writeBackward 

这个函数是尾递归,它最后的递归调用仅用修改了的参数重复函数的操作。可以用迭代来执行这个重复操作,这将更直接、更有效。writeBackward的迭代定义如下:
 

  1. /** Iterative version. */  
  2. void writeBackward(string s)  
  3. {  
  4. int length = s.size();  
  5. while (length > 0)  
  6. {  
  7. cout << s.substr(length - 1, 1);  
  8. length--;  
  9. }           // end while  
  10. } // end writeBackward 

注释:修改尾递归通常非常直接

由于尾递归函数比相应的迭代函数效率低,而且将尾递归函数转换为等效迭代函数的步骤很机械,因此一些编译器将尾递归自动替换为迭代。消除其他形式的递归要复杂些,但是如果有必要的话,还得去做。

有些递归算法(例如rabbit)本身效率不高,而另一些递归算法(例如折半查找3)则效率极高。在涉及算法分析的高级课程中,可了解到如何确定递归算法的相对效率。第10章将简要介绍一些此类技术。

第5章将继续讨论递归,分析一些有直接递归解决方案的难题。当然,本书其他章节也会用到递归。

问题11 判断下面这些在本章出现过的递归函数哪些是尾递归:fact、writeBackward、writeBackward2、rabbit、游行队伍问题中的P、getNumberOfGroups、maxArray、binary- Search和kSmall。
 

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇2.6.3 从n个事物中选出k个 下一篇一致性hash的C++实现

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

·HTTPS 详解一:附带 (2025-12-26 02:20:37)
·TCP/IP协议到底在讲 (2025-12-26 02:20:34)
·TCP和UDP在socket编 (2025-12-26 02:20:32)
·有没有适合新手练习 (2025-12-26 01:48:47)
·用清华镜像网怎么下 (2025-12-26 01:48:44)