【问题标题】:What is a practical difference between a loop and recursion循环和递归之间的实际区别是什么
【发布时间】:2011-03-06 06:10:24
【问题描述】:

我目前正在使用 PHP,所以这个示例将使用 PHP,但问题适用于多种语言。

我正在和我的一个恶魔一起做这个项目,而且一如既往地遇到了一个大问题。现在我们都回家了,无法解决问题。那天晚上我们都找到了解决办法,只是我用循环解决问题,他用递归。

现在我想告诉他循环和递归之间的区别,但我想不出一个解决方案,你需要在正常循环上进行递归。

我将制作两者的简化版本,希望有人能解释一下两者有何不同。

请原谅我的任何编码错误

循环:

printnumbers(1,10);

public function printnumbers($start,$stop)
{
    for($i=$start;$i<=$stop;$i++)
    {
        echo $i;
    }
}

现在上面的代码只是简单地打印出数字。

现在让我们用递归来做这件事:

printnumbers(1,10);

public function printnumbers($start,$stop)
{
    $i = $start;
    if($i <= $stop)
    {
        echo $i;
        printnumbers($start+1,$stop);
    }
}

上面的这个方法将做与循环完全相同的事情,但只有递归。

谁能向我解释一下使用其中一种方法有什么不同。

【问题讨论】:

  • 嗯,你为什么在第二个代码sn-p中倒数?
  • 哦,抱歉,这只是一个错字,已修复!
  • 我认为这表明了对递归函数保持警惕的一个原因:它们很难正确处理,有时会导致这个站点。

标签: recursion loops


【解决方案1】:

堆栈溢出。

不,我不是指网站或其他东西。我的意思是“堆栈溢出”。

【讨论】:

    【解决方案2】:

    对于这样的扁平结构,您真的不需要递归。我使用递归的第一个代码涉及管理物理容器。每个容器可能包含东西(它们的列表,每个都有权重)和/或更多具有权重的容器。我需要一个容器的总重量和它所容纳的所有重量。 (我用它来预测装满露营装备的大背包的重量,而不用打包和称重。)这很容易用递归实现,而循环则要困难得多。但是,许多自然适合一种方法的问题也可以用另一种方法解决。

    【讨论】:

      【解决方案3】:

      通常,使用递归更容易表达问题。当您谈论树状数据结构(例如目录、决策树...)时尤其如此。

      这些数据结构本质上是有限的,因此大多数时候使用递归处理它们会更清晰。

      当堆栈深度通常是有限的,并且每个函数调用都需要一块堆栈时,当谈到可能无限的数据结构时,您将不得不放弃递归并将其转换为迭代。

      特别是函数式语言擅长处理“无限”递归。命令式语言专注于类似迭代的循环。

      【讨论】:

      • +1 感谢静态的实际区别,树状结构。我可以想象递归对于这种情况是多么理想,因为您可以轻松地跳到树中的另一个点。
      • @xtofi 很好的解释!您(或其他人)能否详细说明或指向我一些文献或链接以详细说明您的最后陈述? “特别是函数式语言擅长处理‘无限’递归。命令式语言专注于类似迭代的循环。”
      • @Anovative:寻找“尾递归”或“尾调用优化”。
      • @xtofl 感谢您的快速回复!一个很好的起点,非常有趣。如果我没记错的话,这将在 JavaScript ES6 中得到支持...?
      • 看来是这样。我不知道。 2ality.com/2015/06/tail-call-optimization.html
      【解决方案4】:

      递归有点慢(因为函数调用比设置变量慢),并且在大多数语言的调用堆栈上使用更多空间。如果你尝试printnumbers(1, 1000000000),递归版本可能会抛出 PHP 致命错误甚至 500 错误。

      在某些情况下递归是有意义的,比如对树的每个部分都做某事(获取目录及其子目录中的所有文件,或者可能会弄乱 XML 文档),但它有其代价——速度,堆栈占用空间,以及确保它不会被卡住一遍又一遍地调用自己直到它崩溃所花费的时间。如果循环更有意义,那绝对是要走的路。

      【讨论】:

      • 递归并不总是较慢。速度取决于使用的语言实现。
      • 唯一不慢的情况是语言/编译器可以进行尾调用优化。即使那样,我也从未见过它更快。函数调用涉及存储参数(或指向参数的指针)、推送返回地址和跳转到函数。那是在支持函数的任何语言中,因为这几乎就是函数的本质——具有定义入口点的指令列表,(可选)一种获取参数的方法(在支持递归,几乎需要一个堆栈),以及一种返回的方法。相比之下,循环执行存储和跳转。
      • 尾调用优化只是变相的跳跃。递归甚至 可能 比循环更快的唯一方法是,如果您的语言阻碍循环——在这种情况下,我无论如何都不会使用它。
      【解决方案5】:

      循环会更快,因为执行额外的函数调用总是有开销。

      学习递归的一个问题是很多给出的例子(比如阶乘)都是使用递归的坏例子。

      在可能的情况下,坚持使用循环,除非您需要做一些不同的事情。使用递归的一个很好的例子是循环遍历具有多级子节点的树中的每个节点。

      【讨论】:

      • +1 表示:“在可能的情况下,坚持使用循环,除非您需要做一些不同的事情”。我从来没有真正理解递归的全部目的,这就是为什么我不知道使用递归解决方案是否会更好。现在我知道递归是解决循环不够用的问题的方法。
      • @SaifBechan:我不同意使用循环的教条;递归通常比循环更能反映问题,试图强制将递归问题融入迭代通常会导致代码复杂且难以理解(反之亦然)。
      • 没有人谈论教条。在大多数情况下,您可能不需要递归。
      • 递归在很少需要的地方增加了复杂性。如果你正在处理一棵树,当然可以。递归是很自然的选择。但在大多数其他情况下,循环会完成这项工作并且同时更容易理解。
      【解决方案6】:

      一般来说,递归函数会消耗更多的堆栈空间(因为它实际上是一大组函数调用),而迭代解决方案不会。这也意味着迭代解决方案通常会更快,因为。

      我不确定这是否适用于像 PHP 这样的解释语言,但解释器可能会更好地处理这个问题。

      【讨论】:

        【解决方案7】:

        循环和递归在许多方面是等价的。没有程序需要其中一种,原则上您可以随时从循环转换为递归,反之亦然。

        递归更强大,因为要将递归转换为循环可能需要您自己操作的堆栈。 (尝试使用循环遍历二叉树,你会感到痛苦。)

        另一方面,许多语言(和实现),例如 Java,并没有正确实现尾递归。尾递归是您在函数中做的最后一件事是调用自己(如您的示例中)。这种递归不必消耗任何堆栈,但在许多语言中它们会消耗,这意味着您不能总是使用递归。

        【讨论】:

        • 那些不支持尾递归的语言的问题是堆栈通常相当小 - 导致大型结构上的堆栈溢出。在 Java 或 C# 语言中,您应该使用显式堆栈而不是递归(在大型或未知结构上)。
        • 我从没想过这两者之间的差异如此之小。只有一些语言功能问题和性能问题。我以为会有很多实际差异,但我想我错了。
        【解决方案8】:

        好吧,我不了解 PHP,但大多数语言都会为每次递归生成一个函数调用(在机器级别)。所以它们有可能使用大量的堆栈空间,除非编译器产生尾调用优化(如果你的代码允许的话)。 从这个意义上说,循环更“有效”,因为它们不会增加堆栈。递归的优点是能够更自然地表达一些任务。

        在这种特定情况下,从概念(而不是实施)的角度来看,这两种解决方案是完全等价的。

        【讨论】:

          【解决方案9】:

          与循环相比,函数调用有其自身的开销,例如分配堆栈等。在大多数情况下,循环比递归对应物更容易理解。

          此外,如果 start 和 stop 之间的差异很大并且此代码同时运行的实例太多(当您​​获得更多流量时可能会发生这种情况),您最终将使用更多内存,甚至可能耗尽堆栈空间.

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2023-03-08
            • 2021-06-12
            • 1970-01-01
            • 2010-10-22
            • 1970-01-01
            • 2012-05-08
            • 2020-02-16
            • 1970-01-01
            相关资源
            最近更新 更多