Java复杂性的两种递归方法

时间:2015-03-26 20:54:30

标签: java recursion methods complexity-theory

public static String rec1 (String s) {

    int n = s.length()/2; 
    return n==0 ? s : rec1(s.substring(n)) + rec1(s.substring(0,n)); 
}


public static String rec2 (String s) {
    return s.length()<=1 ? s : rec2(s.substring(1)) + s.charAt(0); 
}

为什么rec2的复杂性大于rec1

我已经对每个进行了10.000次迭代,并使用System.nanoTime()测量执行时间,结果如下:

rec1: Stringlength: 200  Avgtime: 19912ns Recursive calls: 399

rec1: Stringlength: 400  Avgtime: 42294 ns Recursive calls: 799

rec1: Stringlength: 800  Avgtime: 77674 ns Recursive calls: 1599

rec1: Stringlength: 1600 Avgtime: 146305 ns Recursive calls: 3199

rec2: Stringlength: 200  Avgtime: 26386 ns Recursive calls: 200

rec2: Stringlength: 400  Avgtime: 100677 ns Recursive calls: 400

rec2: Stringlength: 800  Avgtime: 394448 ns Recursive calls: 800

rec2: Stringlength: 1600 Avgtime: 1505853 ns Recursive calls: 1600

因此,在1600的强度下,rec1比rec2快10倍。我正在寻求简短的解释。

enter image description here

3 个答案:

答案 0 :(得分:3)

根据Time complexity of Java's substring()String#substring现在会复制支持数组,因此时间复杂度为O(n)

使用这一事实可以看出rec1的时间复杂度为O(n log n),而rec2的时间复杂度为O(n^2)

从最初的String s = "12345678"开始。为简单起见,我将长度视为2的幂。

rec1

  1. s分为"1234""5678"
  2. 这些内容分为"12""34""56""78"
  3. 这些内容分为"1""2""3""4""5""6""7",{ {1}}
  4. 这里有3个步骤,因为"8"。每个步骤都会复制log(8) = 3,因此复制的字符总数为char。当以相反的顺序重新组装O(n log n)时,上面的String现在使用连接连接在一起,使用以下步骤:

    1. 字符串已加入Strings"21""43""65"
    2. 字符串已加入"87""4321"
    3. 加入字符串以制作"8765"
    4. 这又复制了"87654321"个字符!

      <强> O(n log n)

      1. rec2分为s"1"
      2. "2345678"分为"2345678""2"
      3. "345678"分为"345678""3"
      4. "45678"分为"45678""4"
      5. "5678"分为"5678""5"
      6. "678"分为"678""6"
      7. "78"分为"78""7"
      8. 这是共"8"个复制的字符。如果您了解代数,则一般会复制8 + 7 + 6 + 5 + 4 + 3 + 2 = 35个字符,因此(n * (n+1)) / 2 - 1

        如果全部按相反顺序组装,则复制字符数将再次为O(n^2)

答案 1 :(得分:3)

(这是关于时间复杂度的更正版本)

虽然递归次数在n中实际上是线性的(因为递归在每个级别被称为两次),但就复制字符而言,这两种方法之间存在差异。

每个方法都在内部执行两个复制操作 - 一个用于substring(在Java 7中),另一个用于concat(由+运算符表示)。 / p>

rec2中,它一次又一次地复制字符串的右侧,直到只剩下一个字符。因此,字符串中的最后一个字符被复制 depth 次,深度是线性的。所以线性步骤乘以线性副本(实际上是一系列)得到O(n 2 )。

rec1中,每个字符都被复制到左子字符串或右子字符串。但是没有任何字符被复制超过深度次 - 直到我们到达单字符子串。所以每个字符都被复制了n次。尽管递归被调用了两次,但它不会在相同的字符上调用,因此双重调用导致的日志取消不会影响每个字符的副本数。

重建也是如此。相同的副本反过来。

副本数 - n个字符乘以log n的 depth ,得到O(n log n)。执行的步数 - O(n),因此步数不如复制数重要,复杂度总和为O(n log n)。


此外,还有空间复杂性。 rec1在其递归中转到O(log n)的深度,也就是说,它占用O(log n)的堆栈空间。它这样做了两次,但这并没有改变大O.相反,rec2的深度为O(n)。

在我的机器上,使用长度为16384的字符串运行这两个方法会导致rec2的堆栈溢出。 rec1完成没有问题。当然,这取决于您的JVM设置,但是您可以了解情况。

答案 2 :(得分:0)

让我们调查性能差异:

String.substring() 
  • substring在java中非常便宜(直到Java 7 Update 6),因为它不会复制原始数据,只会更新同一阵列上的偏移量。

字符串覆盖+运算符

  • 此处出现差异导致覆盖+运算符在非文字字符串的情况下使用StringBuilder。如果您深入了解StringBuilder.append()方法的实现,您最终会找到对System.arraycopy()的调用。

所以不同之处在于System.arraycopy()处理rec1中指数级减小的数组大小,而rec2中只有线性减小的数组大小。