【问题标题】:Which recursive functions cannot be rewritten using loops? [duplicate]哪些递归函数不能用循环重写? [复制]
【发布时间】:2010-10-06 14:38:35
【问题描述】:

据我所知,大多数递归函数都可以使用循环来重写。有些可能比其他的更难,但大多数都可以重写。

在什么情况下使用循环重写递归函数是不可能的(如果存在这种情况)?

【问题讨论】:

标签: recursion


【解决方案1】:

我不知道递归函数不能转换为迭代版本的例子,但不切实际或效率极低的例子是:

  • 树遍历

  • 快速傅里叶

  • 快速排序(以及其他一些 iirc)

基本上,任何你必须开始跟踪无限潜在状态的地方。

【讨论】:

    【解决方案2】:

    SICP 中,作者挑战读者想出一种纯粹的迭代方法来实现“计数变化”问题(here's an example 来自 Project Euler)。

    但是已经给出了对您问题的严格答案 - 循环和堆栈可以实现任何递归。

    【讨论】:

      【解决方案3】:

      当您递归使用函数时,编译器会为您处理堆栈管理,这使递归成为可能。任何可以递归执行的操作,都可以通过自己管理堆栈来完成(对于间接递归,您只需确保不同的函数共享该堆栈)。

      所以,不,没有什么可以用递归完成,也不能用循环和堆栈完成。

      【讨论】:

      • 我有一个相关的问题:所有递归函数都可以表示为一个单循环吗?
      • @Abhinav:很抱歉回复了一个非常老的线程,但这引起了我的注意,并且有一个简单的证据表明答案是肯定的:图灵机通过执行单个循环来完成它所做的一切:1 . 获取一条指令, 2. 评估它, 3. 转到 1.
      • @VickyChijwani:mokus 的证明对于他提到的范围来说是完全完整的,而且不那么令人困惑的是,他可以说,“所有程序和子例程都在一个简单的单循环中执行......”,所以, 递归是一种处理堆栈管理的抽象,类似于任何更高级别的编程控制构造是处理器管道最终将执行的操作、获取指令并执行它们的抽象。因此,在某种程度的抽象被移除后,所有程序都是一个循环。
      • 大部分是真的。换句话说,这不是真的。有些问题不能在循环中完成,它们必须是递归的。很少见,但确实存在youtube.com/watch?v=i7sm9dzFtEI
      • @ChrisHuang-Leaver 您可能还没有搜索过“如何将 akermann 编写为循环”。 stackoverflow.com/questions/5605258/…
      【解决方案4】:

      每个递归函数都可以用一个循环来实现。

      想想处理器做了什么,它在一个循环中执行指令。

      【讨论】:

      • 实际上它不能作为循环工作。现代 CPU 中的管道更像是一条装配线。从指令一开始,转到指令指针++ 上的下一条指令。有些指令会修改指令指针本身,从而导致出现循环或跳转。
      • 不仅仅是数据。大多数分支预测缓存会根据指针运行位置和先前的未来指令。虽然它可以通过组装进行修改,但它是处理器的基本组成部分。
      【解决方案5】:

      间接递归仍然可以转换为非递归循环;只需从其中一个函数开始,然后内联对其他函数的调用,直到你有一个直接递归函数,然后可以将其转换为使用堆栈结构的循环。

      【讨论】:

        【解决方案6】:

        一个极难从递归转换为迭代的例子是Ackermann function

        【讨论】:

        • 很好的例子。但还有一个问题:这是不可能的,还是非常困难?
        • 如果您知道一般技术,也不会太难。
        • 我尝试过这样做,对我来说似乎并不难。检查这段代码(请告诉我它有什么问题):...
        • 推(米);推(n); while (stackSize > 1) { n = pop(); m = 流行();如果 (m == 0) 推(n+1);否则 if (m > 0 && n == 0) { push(m-1);推(1); } else if (m > 0 && n > 0) { push(m-1);推(米);推(n-1); } }
        【解决方案7】:

        任何递归函数都可以进行迭代(进入循环),但您需要自己使用堆栈来保持状态。

        通常情况下,尾递归很容易转化为循环:

        A(x) {
          if x<0 return 0;
          return something(x) + A(x-1)
        }
        

        可以翻译成:

        A(x) {
          temp = 0;
          for i in 0..x {
            temp = temp + something(i);
          }
          return temp;
        }
        

        其他可以翻译成尾递归的递归也很容易改变。另一个需要更多的工作。

        以下内容:

        treeSum(tree) {
          if tree=nil then 0
          else tree.value + treeSum(tree.left) + treeSum(tree.right);
        }
        

        翻译不是那么容易。您可以删除递归的一部分,但如果没有保持状态的结构,则另一部分是不可能的。

        treeSum(tree) {
          walk = tree;
          temp = 0;
          while walk != nil {
            temp = temp + walk.value + treeSum(walk.right);
            walk = walk.left;
          }
        }
        

        【讨论】:

        • 您原来的尾递归示例并不完全是尾递归的(但仍然说明了“线性”递归通常很容易翻译,而更高的数量通常不那么容易)。
        • 谢谢。最后一个例子似乎是我正在寻找的。去掉递归真的不可能吗?
        • 不,你总是可以用循环重写它。转换成使用延续的代码几乎是机械的,可以用 F# 等语言编译成循环(不使用堆栈),参见例如lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!256.entry
        • 谢谢。您的回答非常好,但我选择了 Carl 的回答,因为它更简单(这对线程的新读者很有帮助)。
        【解决方案8】:

        与其说它们不能使用循环来实现,不如说是递归算法的工作方式,它更清晰、更简洁(在许多情况下可以通过数学证明)一个函数是正确的.

        很多递归函数可以写成尾循环递归,可以优化为循环,但这取决于算法和使用的语言。

        【讨论】:

          【解决方案9】:

          您始终可以使用循环,但您可能需要创建更多数据结构(例如模拟堆栈)。

          【讨论】:

            【解决方案10】:

            它们都可以写成一个迭代循环(但有些可能仍然需要一个堆栈来保持以前的状态以供以后的迭代使用)。

            【讨论】:

              猜你喜欢
              • 2011-07-16
              • 2021-12-06
              • 2012-11-01
              • 2018-06-24
              • 1970-01-01
              • 2011-01-13
              • 2023-03-24
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多