与使用堆栈相比,递归通常被认为是一种过时的遍历方法吗?

Posted

技术标签:

【中文标题】与使用堆栈相比,递归通常被认为是一种过时的遍历方法吗?【英文标题】:Is recursion generally considered to be an outdated method of traversing compared to using a stack? 【发布时间】:2010-10-22 04:58:33 【问题描述】:

我一直在阅读一些人们选择使用堆栈而不是递归的地方。这是因为递归被视为完成工作的一种过时方式,还是两种方法都同样适用于不同的上下文?

【问题讨论】:

【参考方案1】:

没有。恰好相反。递归是表达一整类问题的自然方式。如果没有递归,堆栈是一种模拟方式。

例如,请参阅此question。或者考虑一种标准递归函数:计算第 n 个斐波那契数。

你会记得Fibonacci numbers是系列

0,1,1,2,3,5,8,13, ...

定义为 Fn = Fn-1+Fn-2

这可以写成递归定义

基本情况: F(0) = 0 F(1) = 1 递归步骤: F(n) = F(n-1)+F(n-2)

所以,你有 F(0) = 0,F(1)= 1,F(2)=F(0)+F(1)=1,等等。

计算这个的简单程序(在 C 中只是为了笑)是:

int fib(int n) 
    /* we'll ignore possible negative arguments, see Wikipedia */
    switch(n) 
       case 0: return 0; break;
       case 1: return 1; break;
       default: return fib(n-1)+fib(n-2); break;
    

注意该程序与原始定义的对应程度有多接近?

问题是,C 管理调用堆栈中的所有中间结果。有些语言没有被定义为这样做(我能想到的唯一一种是旧的 FORTRAN,但我相信还有其他的)。如果您使用汇编程序或旧 FORTRAN 编写,那么您必须管理自己的堆栈以跟踪这些中间结果。

【讨论】:

我同意你关于递归是表达问题的“自然方式”(并相应地支持你)的观点。但是,我希望看到一些认可,即它的计算成本更高一些,因此也受到了 tpdi 的支持。 除非那不是真的。对于某些问题和某些环境,它的计算成本更高。例如,这个程序非常昂贵。另一方面,如果做更多的工作,它可以表示为尾递归,如下所示:triton.towson.edu/~akayabas/COSC455_Spring2000/…,尾递归并不比迭代差,而且通常更好,参见portal.acm.org/citation.cfm?id=800055.802039【参考方案2】:

迭代通常比递归更快/开销更少。通过递归,我们隐式地使用机器的堆栈作为我们的堆栈——我们“免费”获得它——但我们付出了昂贵的函数调用(以及随之而来的机器堆栈管理)的成本。

但递归函数通常更易于书写和阅读。

通常,可以使用递归来编写函数,直到它成为瓶颈,然后将其替换为使用显式堆栈的迭代函数。

【讨论】:

+1 - 良好的观察力。正如查理所说,有些问题对于递归来说是非常自然的。但是,您最好指出开发人员需要了解他们正在做出的权衡。 除了不一定如此:这是一个老妇人的故事。请参阅 Guy Steele 的论文:portal.acm.org/citation.cfm?id=800055.802039 @Charlie Martin:最安全的说法可能是:这取决于,因为无法预测编译器/解释器的实现类型。我确信 Lisp 中的递归更快,Lisp 中的一切都是递归,如果不是更快,那将是一个严重的问题。与往常一样,这取决于您,如果您真的想知道什么更快,请对其进行基准测试。 那篇论文并没有真正进行公平的比较。它真正说的是,经过编译器优化的递归算法比实现不佳的迭代算法要好。但在这一点上,它只是比较两种迭代算法(编译器的输出是迭代的),当然实现良好的算法更好。【参考方案3】:

更新以包含鱼唇修正。

使用堆栈是消除recursion 的标准技术

另见:What is tail-recursion?

尾递归的一个例子(可以使用迭代删除):

public class TailTest
   
    public static void Main()
           
        TailTest f = new TailTest();
        f.DoTail(0);
    

    public void DoTail(int n)
           
        int v = n + 1;      
        System.Console.WriteLine(v);    

        DoTail(v);   // Tail-Recursive call
    

【讨论】:

每一种递归都可以通过利用栈结构迭代地重写。递归是一种利用调用栈来解决问题的方法。但是,尾递归可以使用 GOTO 重写,本质上是将它们转换为迭代循环。这是消除尾递归的标准方法。【参考方案4】:

如果您所处的编程语言/环境中尾调用会增加堆栈(未应用尾调用优化 (TCO)),那么最好避免深度递归,并且首选可能使用堆栈数据结构的迭代解决方案.

另一方面,如果您处于支持尾调用迭代的语言/环境中,或者如果递归深度总是很小,那么递归通常是一个很好/优雅的解决方案。

(这有点过于宽泛,但总的来说,我绝不会称递归“过时”。)

【讨论】:

【参考方案5】:

不不,我认为现代开发人员应该强调几毫秒内的可读性和易于维护。

如果问题是递归的,我完全推荐你的递归解决方案。

此外,您最终可能会引入一些意想不到的错误,试图强制执行迭代/堆叠解决方案。

【讨论】:

你把钉子钉在了头上。您必须根据任务选择正确的工具。但大多数情况下,可读性比在固定点中表达问题更重要。 我同意,只要清楚您的工作必须满足与客户协商的要求即可。如果他们需要您减少程序执行时间,那么您需要检查您的实施选择。

以上是关于与使用堆栈相比,递归通常被认为是一种过时的遍历方法吗?的主要内容,如果未能解决你的问题,请参考以下文章

二叉树的各种遍历方式,我都帮你总结了,附有队列堆栈图解

基础篇8 # 递归:如何避免出现堆栈溢出呢?

为啥哈希表键通常被认为是无序的?

同学,二叉树的各种遍历方式,我都帮你总结了,附有队列堆栈图解(巩固基础,强烈建议收藏)

JavaScript的数组实现队列与堆栈的方法

php递归遍历文件夹