【问题标题】:Recursion vs. Stack implementation. Why does recursion return StackOverflow when Stack does not?递归与堆栈实现。为什么当 Stack 不返回时递归返回 StackOverflow?
【发布时间】:2014-04-22 00:46:39
【问题描述】:

这个问题的依据:

我将在今年夏天毕业并获得计算机科学学位,而且我从未有过教授强调堆栈的重要性。然而,我有多个项目都专注于递归的使用。我发现递归有用且令人兴奋,并且在我的个人项目中经常使用它。

我最近参加了一次工作面试,面试官对他们问题的递归解决方案非常失望。他们想要 Stack 解决方案。我做了很多研究,但我仍然不确定何时使用哪个。

给出以下演示:

public class TestCode {

    static long startTime = 0;
    static long stopTime = 0;
    static long totalTime = 0;

    public static void main(String[] args) throws IOException {
        int x = 10000;
        startTime = System.currentTimeMillis();
        recursiveMethod(x);
        System.out.println();
        stopTime = System.currentTimeMillis();
        totalTime = stopTime - startTime;
        System.out.println(totalTime);
        System.out.println();

        startTime = System.currentTimeMillis();
        stackMethod(x);
        System.out.println();
        stopTime = System.currentTimeMillis();
        totalTime = stopTime - startTime;
        System.out.println(totalTime);
        }

    public static void recursiveMethod(int a){
        if(a >= 0){
            recursiveMethod(a - 1);
            System.out.println("Recursion: " + a + " ");
        }
    }

    public static void stackMethod(int a){
        Stack<Integer> myStack = new Stack<Integer>();
        while(a >= 0){
            myStack.push(a);
            a--;
        }
        while(myStack.isEmpty() == false){
            a = myStack.pop();
            System.out.println("Stack: " + a + " ");
        }
    }
}

两种解决方案都在大约 200 毫秒内完成。通过添加零来更改 x 的值:x = 100000 给我一个 StackOverflowError(在递归方法上)。

当我用 x 的 SAME 值注释掉递归解决方案时,程序成功运行,这意味着 Stack 解决方案的工作远远超出了递归解决方案的限制。

问题

  • 为什么递归解决方案会产生与 Stack 解决方案相同的迭代次数的 StackOverflowError,但 Stack 解决方案不会出错?
  • 如果 Stack 解决方案功能更强大且使用更少的内存,您何时会使用递归解决方案?
  • 在选择其中一个之前必须考虑的递归和堆栈/迭代解决方案之间的根本区别是什么?

【问题讨论】:

  • 递归通常为算法提供清晰的“数学”描述。然而,务实地讲,在 Java 中使用递归可能会导致未知(和大量)内存使用,当您直接控制信息时(例如在“堆栈”解决方案中),这可能会更有效。
  • Java 没有尾调用优化,因此递归会占用有限的堆栈空间。栈解决方案使用堆空间。
  • (与@Kevin 声明相关的讨论:mail.openjdk.java.net/pipermail/mlvm-dev/2010-October/…
  • 我希望您能详细说明面向堆栈的解决方案的性质,或者提出了什么问题?这些堆栈解决方案在本质上是否等同于/模拟递归解决方案的函数调用堆栈上发生的事情,还是它们有什么不同?
  • @mvw 提出的问题与树结构有关,从字符串生成树。给定 a(bcd(efg)) 创建一棵树而不使用递归。我个人遇到这个问题是因为解决了CodeEval 上的机器人运动挑战。我在那里使用了递归,但我决定尝试使用多个堆栈找到一个非递归解决方案,看看哪个更有效。

标签: java performance recursion stack


【解决方案1】:

为什么递归解决方案会产生与 Stack 解决方案相同迭代次数的 StackOverflowError,但 Stack 解决方案不会出错?

递归使用你的线程堆栈,它有一个低得多的限制。 STack 使用堆,这要大得多,但最终会出现 OutOfMemoryError。

如果 Stack 解决方案功能更强大且使用更少的内存,您何时会使用递归解决方案?

它更慢、更难编写、更容易出错并且使用更多内存。只是限制要高很多。

我看到很多人在堆栈上使用递归,这是因为递归“更容易”,但效率要低得多? (本例中的 3 行代码与 7 行代码)。

您还没有预热代码来说明哪个更有效。我会忽略前 10,000 次迭代或运行时间的第一秒。

虽然我知道你不能说为什么其他人都使用递归,

通常递归比使用迭代效率低,但是有些问题不能很容易地重构为使用递归。 95% 的问题在 Java 中使用迭代效率更高。

我想质疑它的用途而不是使用它,因为如果没有充分的理由,“其他人都会这样做”。

大多数时候人们使用递归是因为它更简单、更快,但它可能会耗尽堆栈空间,在极少数情况下,您必须使用比堆栈更有效的堆栈或 ArrayList。

【讨论】:

  • 这里是一个具体的例子,作者描述了他在递归与迭代的简单算法方法中遇到的一些困难:blog.andreloker.de/post/2009/03/10/…
  • @JasonC +1 任何类型的树或图都更容易使用递归,但是当 JDK 中内置了树和 SkipList 时,编写自己的代码来导航节点是相当罕见的。更罕见的是,您的图表如此之深以至于耗尽了您的筹码。
  • @PeterLawrey 树是递归数据结构,所以很自然。
  • @mvw 是的,但是当您使用 TreeMap 或 ConcurrentSkipListMap 时,您通常使用迭代而不是递归,并且对于许多开发人员来说,这些是他们使用的唯一一种树。
  • @leigero 递归在深度为问题空间的ln N 顺序时效果很好,例如。对于平衡树,您需要的深度是元素数量的对数,因此它永远不会失控。对于图表,您会得到类似的关系。如果您的深度是订单N,那么您已经发现了可扩展性问题。
【解决方案2】:

递归为许多问题提供了易于阅读的简单解决方案,但是它存在与其他人指出的函数堆栈大小限制相关的问题。

我个人对此的理论是,如果你的递归深度是 O(log n) ,你可以使用递归。

为了透视平衡二叉树的遍历顺序是 +/- O(log n) 递归深度(虽然这可以通过堆栈轻松完成,但我在这里以它为例)

但是如果你的递归深度不能被限制在 O(log n) 内,很有可能会耗尽函数堆栈空间。如果您尝试向flood fill 编写程序,则不能使用递归。

【讨论】:

    【解决方案3】:

    大多数机器架构使用固定大小的堆栈,因此您的递归算法的工作内存量有限。但是,您分配的显式堆栈可以增长到主内存的大小;因此它有更多的工作空间。结合递归堆栈每帧必须包含比显式堆栈更多的数据这一事实,您可以看到为什么堆栈实现更有效。

    递归算法通常看起来更优雅;但由于这些原因,基于堆栈的算法通常是更好的选择。只有当递归层数严格限制时,才应考虑使用递归算法。

    【讨论】:

      【解决方案4】:

      要记住一点:从数学和信息技术的角度来看,递归和迭代是等价的。

      【讨论】:

        【解决方案5】:

        为什么递归解决方案会产生与 Stack 解决方案相同迭代次数的 StackOverflowError,但 Stack 解决方案不会出错?

        因为递归比使用堆栈使用更多的内存?它在哪里说它们应该在相同的迭代次数下都失败?

        如果 Stack 解决方案功能更强大且使用更少的内存,您何时会使用递归解决方案?

        首先,给我看一个“堆栈解决方案更强大”的案例。这不是一个有意义的问题。你的第三个也不是。许多算法的递归版本更容易(a)编写和(b)理解。例如,考虑调车场算法与递归下降表达式解析器。

        【讨论】:

        • 这不仅仅是它使用“更多内存”;它使用不同且更有限的内存区域。递归也不一定必须使用更多内存(例如,考虑深度优先遍历大树,堆栈方法会消耗待处理节点的内存,而递归方法则不会)。
        猜你喜欢
        • 1970-01-01
        • 2019-11-04
        • 2013-01-29
        • 1970-01-01
        • 1970-01-01
        • 2022-01-03
        • 2018-08-05
        • 2011-04-29
        • 1970-01-01
        相关资源
        最近更新 更多