【问题标题】:Do iterative and recursive versions of an algorithm have the same time complexity?算法的迭代和递归版本是否具有相同的时间复杂度?
【发布时间】:2011-12-16 09:27:41
【问题描述】:

比如说,斐波那契数列的迭代和递归版本。它们具有相同的时间复杂度吗?

【问题讨论】:

  • 有几种迭代算法用于计算斐波那契数列和几种递归算法,复杂度各不相同。
  • 我认为您可以合理地说,如果它们没有相同的复杂性,那么它们就不是“相同算法”的迭代和递归版本。它们是不同的算法,当然计算相同结果的不同算法不一定具有相同的复杂性。也就是说,用相同的名称来指代相关算法组是很常见的。例如,快速排序的行为会有所不同,具体取决于您选择枢轴的方式以及处理分区两侧的顺序,但所有可能性通常都称为“快速排序”。
  • ... 由此可见,是否可以将两位代码描述为“相同的算法”在某些情况下取决于您的语言/编译器是否实现尾递归。如果递归版本创建了一个它不需要的调用堆栈,那么它是一种空间复杂度较低的不同算法。
  • @SteveJessop:还有一个可能的崩溃错误 :)

标签: algorithm complexity-theory time-complexity


【解决方案1】:

答案很大程度上取决于您的实施。对于您给出的示例,有几种可能的解决方案,我会说实现解决方案的天真方法在迭代实现时具有更好的复杂性。以下是两个实现:

int iterative_fib(int n) {
   if (n <= 2) {
     return 1;
   }
   int a = 1, b = 1, c;
   for (int i = 0; i < n - 2; ++i) {
     c = a + b;
     b = a;
     a = c;
   }
   return a;
}
int recursive_fib(int n) {
  if (n <= 2) {
    return 1;
  }
  return recursive_fib(n - 1) + recursive_fib(n-2);
}

在这两种实现中,我都假设了正确的输入,即 n >= 1。第一个代码要长得多,但它的复杂度是 O(n),即线性,而第二个实现更短,但具有指数复杂度 O(fib(n) ) = O(φ^n) (φ = (1+√5)/2) 因此要慢得多。 可以通过引入记忆(即记住您已经计算的函数的返回值)来改进递归版本。这通常是通过引入一个存储值的数组来完成的。这是一个例子:

int mem[1000]; // initialize this array with some invalid value. Usually 0 or -1 
               // as memset can be used for that: memset(mem, -1, sizeof(mem));
int mem_fib(int n) {
  if (n <= 2) {
    return mem[n] = 1;
  }
  if (mem[n-1] == -1) {
    solve(n-1);
  }
  if (mem[n-2] == -1) {
    solve(n-2);
  }
  return mem[n] = mem[n-1] + mem[n-2];
}

这里递归算法的复杂度和迭代解一样是线性的。我上面介绍的解决方案是您的问题的动态编程解决方案的自上而下的方法。自下而上的方法将导致与我介绍的迭代解决方案非常相似。 在wikipedia中有很多关于动态规划的文章

根据我在经验中遇到的问题,有些问题很难用自下而上的方法(即迭代解决方案)解决,而另一些问题则很难用自上而下的方法解决。 然而,该理论指出,每个具有迭代解决方案的问题都有一个具有相同计算复杂度的递归(反之亦然)。

希望这个答案有帮助。

【讨论】:

  • I would say that the naive way to implement a solution has better complexity when implemented iterative. 我会说迭代版本不再天真了。斐波那契数列的问题在于,编写指数递归版本非常容易,但编写指数迭代版本却很难,因此编写迭代算法时出现的第一个版本并不是很幼稚,你一定已经投入了想出任何迭代的想法。
【解决方案2】:

计算 fibanocci 级数的特定递归算法效率较低。 考虑以下通过递归算法找到fib(4)的情况

                int fib(n) :
                        if( n==0 || n==1 )
                            return n;
                        else
                            return fib(n-1) + fib(n-2)

现在当上述算法执行 n=4 时

                            fib(4)

                    fib(3)             fib(2)

                fib(2)   fib(1)     fib(1)   fib(0)

             fib(1)  fib(0) 

这是一棵树。它说要计算 fib(4),你需要计算 fib(3) 和 fib(2) 等等。

请注意,即使对于较小的值 4,fib(2) 也会计算两次,而 fib(1) 会计算三次。增加的数量会随着数量的增加而增加。

有一个猜想,计算fib(n)所需的加法次数为

                     fib(n+1) -1

所以这种重复是导致该特定算法性能下降的原因。

斐波那契数列的迭代算法要快得多,因为它不涉及计算多余的东西。

但对于所有算法来说,情况可能并不相同。

【讨论】:

    【解决方案3】:

    如果您采用一些递归算法,您可以通过将所有函数局部变量存储在一个数组中来将其转换为迭代,从而有效地模拟堆上的堆栈。如果这样做,迭代和递归之间没有区别。

    请注意,(至少)有两种递归斐波那契算法,因此为了准确地举例,您需要指定您正在谈论的递归算法。

    【讨论】:

      【解决方案4】:

      是的,每个迭代算法都可以转换为递归版本,反之亦然。一种方法是传递延续,另一种方法是实现堆栈结构。这样做不会增加时间复杂度。

      如果您可以优化尾递归,那么每个迭代算法都可以转换为递归算法,而不会增加渐近内存复杂度。

      【讨论】:

        【解决方案5】:

        是的,如果您在算法背后使用完全相同的想法,那没关系。然而,就迭代而言,递归通常很容易使用。例如,编写河内塔的递归版本非常容易。将递归版本转换为相应的迭代版本是困难的并且容易出错,即使可以做到。实际上,有一个定理指出,每个递归算法都可以转换为等效的迭代算法(这样做需要使用一个或多个堆栈数据结构迭代地模拟递归,以保存传递给递归调用的参数)。

        【讨论】:

          猜你喜欢
          • 2023-03-24
          • 2023-03-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-10-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多