【问题标题】:Do recursive functions have a minimum space complexity of O(N)?递归函数的最小空间复杂度是否为 O(N)?
【发布时间】:2013-11-30 11:00:12
【问题描述】:

我在考虑递归函数。举一个简单的函数,比如递归打印一个链表:

void print(list *list){
  if(list){
     cout << list->data
     print(list->next);
  }
}

起初这似乎是一个非常无害的函数,但它不是在每个堆栈帧中存储一个地址(由变量列表标记)吗?假设没有尾调用优化。我们需要 N 个地址的空间,其中 N 是列表的大小。所需空间与列表大小成正比线性增长。

如果没有至少一个局部变量或参数存储在堆栈上,我想不出你将如何实现递归函数。因此,似乎每个递归函数都将具有最好的线性空间复杂度。如果是这种情况,那么使用迭代而不是递归不是几乎总是可取的吗?

【问题讨论】:

  • 编译器 could trivially optimize this 不会在堆栈中保留任何内容。
  • @DavidSchwartz:当然,但是 OP 在中间段落中声明,“假设没有尾调用优化”。

标签: c++ c algorithm recursion big-o


【解决方案1】:

答案是否定的,例如(完整)二叉树的遍历,空间复杂度为O(log N),即树的深度。

【讨论】:

    【解决方案2】:

    虽然可以假设所有未优化的函数调用都会消耗一个堆栈帧,但对 N 个元素进行操作的递归算法并不总是需要 O(N) 大小的堆栈。

    例如,递归树遍历算法使用 O(lg N) 堆栈帧,递归 QuickSort 也是如此。

    【讨论】:

    • 这是一个非常好的观点。我应该说一个非常量的空间,但问题的精神仍然是一样的。
    【解决方案3】:

    你是对的。您甚至不需要变量,返回地址本身已经占用了空间。有一些方法可以避免递归中的深度嵌套(尾递归),现代编译器在许多情况下会自动执行此操作。但除此之外,在空间复杂度方面,迭代比递归更可取。

    【讨论】:

      【解决方案4】:

      你是对的,假设没有尾调用优化,这段代码的空间复杂度与列表的大小成线性关系。

      一般来说,递归会使事情变得更慢,而且更需要内存,是的。但是你不能总是通过迭代实现来渐近地改进它,因为对于非尾递归函数,你需要在迭代实现中手动维护堆栈,所以你仍然会使用相同数量的内存。

      考虑深度优先遍历。您需要将每个节点以及接下来需要访问的子节点存储在堆栈中,以便在您访问其中一个子节点返回后,您知道接下来要访问哪个节点。递归使这变得非常容易,因为它抽象了所有丑陋的簿记。迭代实现不会渐进地更好,我希望实际差异也非常非常小。

      很多时候,递归在不牺牲任何东西的情况下让事情变得更容易。在您的情况下,它没有意义 - 它只是递归的一个教学示例。

      【讨论】:

      • 大声笑!我一直在评论框中输入问题,在我提交之前,您进行编辑以回答确切的问题。好笑。但我还有一个。您提到递归会牺牲速度。这是为什么呢?
      • @ordinary - 很抱歉,我通常需要大约 5 分钟才能把所有东西都拿出来:P。因为调用函数需要时间,所以会牺牲速度——这并不是一个即时操作。您拥有的参数,返回地址,所有内容都需要添加到隐式堆栈递归创建。这不会影响大哦(好吧,如果您在真正不应该使用递归的地方使用递归,就像在您的示例中那样),但在实践中,会有一个(可能很小)差异。
      • 所以递归本质上是语法糖?它如何影响我的示例(或任何示例)中的 big-o?在这种情况下,big-o 不是还是线性的吗?
      • @ordinary - 它通常不会影响(渐近)运行时间。它会影响使用的空间,就像在您的示例中一样。您发布的内容的迭代实现将具有O(1) 空间复杂度,而您的实现具有O(length of list)。如果你使用正确的递归和/或你的编译器优化了尾递归,这也不会发生。
      猜你喜欢
      • 2021-08-30
      • 1970-01-01
      • 1970-01-01
      • 2017-05-03
      • 2015-05-25
      • 2017-09-04
      • 1970-01-01
      • 1970-01-01
      • 2020-03-22
      相关资源
      最近更新 更多