【问题标题】:Run-time complexities for recursive algorithms递归算法的运行时复杂度
【发布时间】:2012-03-02 21:34:52
【问题描述】:

我搜索了很多,似乎找不到很多与运行时复杂性、递归和 java 相关的材料。

我目前正在我的算法课中学习运行时复杂性和 Big-O 表示法,但我在分析递归算法时遇到了麻烦。

private String toStringRec(DNode d)
{
   if (d == trailer)
      return "";
   else
      return d.getElement() + toStringRec(d.getNext());
}

这是一种递归方法,它将简单地遍历双向链表并打印出元素。

我唯一能想到的是它的运行时复杂度为 O(n),因为递归方法调用的数量将取决于 DList 中的节点数量,但我仍然没有对这个答案感到满意。

我不确定是否应该考虑添加 dd.getNext()

或者我只是完全偏离轨道并且运行时复杂性是恒定的,因为它所做的只是从DList 中的DNodes 检索元素?

【问题讨论】:

标签: java recursion big-o time-complexity tailrecursion-modulo-cons


【解决方案1】:

乍一看,这像是tail recursion modulo cons的经典案例,尾调用的泛化。相当于一个有迭代次数的循环。

然而,事情并不是那么简单:这里的棘手之处在于将d.getElement() 添加到一个不断增长的字符串中:这本身就是一个线性 操作,它会重复N 次.因此你的函数的复杂度是O(N^2)

【讨论】:

  • 嗯,我以为 d.getElement() 是为了获取存储在节点 d 的数据。我猜他需要让他的问题更清楚一点......
  • @小川宇不,d.getElement()O(1)。后面的字符串连接是线性的。
  • 是的,感谢您没有忽略字符串连接的成本。这是完全正确的。高斯和1+2+...+n 发挥作用,这就是二次方的来源。
【解决方案2】:

这是一个非常简单的示例,但诀窍是定义递归关系,它是给定输入大小的运行时间函数,就较小的输入大小而言。对于这个例子,假设在每一步完成的工作花费恒定的时间 C 并假设基本情况没有工作,它将是:

T(0) = 0
T(n) = C + T(n-1)

然后您可以使用替换来求解运行时间以查找系列:

T(n) = C + T(n-1) = 2C + T(n-2) = 3C + T(n-3) = ... = nC + T(n-n) = nC + 0 = nC

根据O的定义,这个方程是O(n)。这个例子不是特别有趣,但是如果你看一下合并排序的运行时间或其他分而治之的算法,你可以更好地了解递归关系。

【讨论】:

  • 当然在这个例子中你也可以理解它:你打印出链表中的每个节点,所以你执行的打印数量的增长速度与名单。所以这是线性时间。
  • 在这个特定的例子中,考虑到 Java 中字符串连接的工作方式,我们不能假设每一步完成的工作都是恒定时间。
  • 我认为做这个假设是没问题的,因为这个问题的重点不是要查找Java库函数的复杂性,而是要了解这种递归算法一般是如何分析的。
  • 我同意制定递归是解决这个问题的关键。但是:我们需要确保我们正在解决正确重复。如果我们实际运行这个程序并在 n 范围内绘制它的行为,我们将观察到 O(n^2) 时间。这需要一个解释,否则我们的分析是没有用的。递归必须修改为T(n) = C*n + T(n-1),因为字符串连接的原始操作在被连接的字符串的大小上是线性的。除非该语言提供字符串绳索,否则我们必须处理字符串上+ 的非恒定成本。
  • 是的,很公平。感谢您向我介绍绳索,我以前从未听说过。
【解决方案3】:

如果 T(n) 是基本操作的数量(在这种情况下 - 当我们进入函数体时,里面的任何行都最多执行一次,并且除了第二次返回之外的所有行都不是 O(1) ) 通过在 n 个元素的列表上调用 toStringRec 来执行,然后

  T(0) = 1  - as the only things that happen is the branch instruction and a
              return
  T(n) = n + T(n-1) for n > 0 - as the stuff which is being done in the
              function besides calling toStringRec is some constant time stuff and
              concatenating strings that takes O(n) time; and we also run
              toStringRec(d.getNet()) which takes T(n-1) time

至此,我们已经描述了算法的复杂性。我们现在可以计算 T 的封闭形式,T(n) = O(n**2)。

【讨论】:

  • 否:“函数中完成的事情”不是 O(1)。假设每个元素的字符串大小不为空,这项工作所花费的时间与n 成正比。因此,对于某个常数 C,T(n) 的封闭形式最终看起来像 T(n) = 1C + 2C + 3C + ... + nC。这是一个高斯和。 T(n) 是二次的,不是线性的。
【解决方案4】:

正如您所建议的,该算法的运行时间复杂度为 O(n)。您的列表中有 n 个项目,并且该算法将为每个项目执行几乎固定的工作量(这些工作是 Element 和 Next 访问,加上一个新的 toStringRec 调用)。从 DNode 中检索元素需要常数时间,而常数时间在 big-O 表示法中被丢弃。

递归方法(在大多数情况下)的有趣之处在于它们的空间复杂度也是 O(n)。每次调用 toStringRec 都会创建一个新的堆栈条目(用于存储传递给方法的参数),该调用被调用 n 次。

【讨论】:

  • 不幸的是,这个解释提供了一个错误的结论。它没有考虑字符串连接的成本。也就是说,每个项目的成本不是恒定的。这是这个问题的一个重点。请更正这一点。
  • 我同意每个项目的成本不是恒定的,但我不同意它是 O(n)。 string1 + string2 的成本是 O(m),其中 m 是结果字符串的长度。具体来说,连接两个字符串在最坏的情况下是创建一个长度为 m 的新 char[] 并从原始字符串中一次复制每个字符。在给定代码的第 n 次迭代时,toStringRec 可能会返回一个很长的字符串,但连接的成本仍然是 O(m)。在本例中,m 与 n 没有直接关联,因为 getElement 可能返回空字符串或非常长的字符串。
  • 假设有一些长度m,这是任何特定 d.getElement() 大小的上限。然后我们从toStringRec(node) 得到的返回字符串的大小受从node 开始的链长度的约束。让T(n) 成为计算成本。然后:T(n) < C1 + C2 * m * (n-1) + C3 *T(n-1),对于一些常量C1,C2,C3。中间项表示字符串连接。让C4 是一个大于C1 的常数,它也是m * C2 的倍数。
  • 抱歉,证明很长。不得不拆分它。 :) 无论如何,所以现在我们说T(n) < C1 + C2 * m * (n-1) + C3 * T(n-1) < C4 + C2 * m * (n-1) + C3 * T(n-1)。由于我们选择 C4 作为C2 * m 的倍数,因此我们考虑。我们现在可以说T(n) < C2 * m * (n + C) + C3 * T(n-1) 一些C。术语C2 * m 也是一个常数。所以我们真的有像T(n) < C1 * n + C2 + C3 * T(n-1) 这样的东西。开始展开重复。对于一些常量C1C2,我们最终会得到T(n) < C1 * (1 + 2 + ... + n) + C2(1 + 2 + ... + n) 是众所周知的高斯和,具有封闭形式 n(n+1)/2。那里!
  • 如果您不相信,请提出问题。一旦你是,相应地编辑你的答案。 :) 祝你好运!
【解决方案5】:

对于这样的递归算法,通常可以编写一个递归方程来计算阶数。习惯上显示用 T(n) 执行的指令数。在这个例子中,我们有:

T(n) = T(n - 1) + O(1)

(假设函数getElement 在恒定时间内运行。)这个方程的解通常是 T(n) = O(n)。

这是一般情况。但是,有时您可以在不编写此类方程式的情况下分析算法。在这个例子中,你可以很容易地争论每个元素最多被访问一次,并且每次都完成一些恒定的时间工作;因此,完成这项工作总共需要 O(n)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-05-28
    • 2011-02-12
    • 1970-01-01
    • 2017-07-15
    • 1970-01-01
    相关资源
    最近更新 更多