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倍。我正在寻求简短的解释。
答案 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
:
s
分为"1234"
和"5678"
。"12"
,"34"
,"56"
,"78"
"1"
,"2"
,"3"
,"4"
,"5"
,"6"
,"7"
,{ {1}} 这里有3个步骤,因为"8"
。每个步骤都会复制log(8) = 3
,因此复制的字符总数为char
。当以相反的顺序重新组装O(n log n)
时,上面的String
现在使用连接连接在一起,使用以下步骤:
Strings
,"21"
,"43"
,"65"
"87"
,"4321"
"8765"
。这又复制了"87654321"
个字符!
<强> O(n log n)
强>
rec2
分为s
和"1"
。"2345678"
分为"2345678"
和"2"
。"345678"
分为"345678"
和"3"
。"45678"
分为"45678"
和"4"
。"5678"
分为"5678"
和"5"
。"678"
分为"678"
和"6"
。"78"
分为"78"
和"7"
。这是共"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()
字符串覆盖+
运算符
+
运算符在非文字字符串的情况下使用StringBuilder
。如果您深入了解StringBuilder.append()
方法的实现,您最终会找到对System.arraycopy()
的调用。所以不同之处在于System.arraycopy()
处理rec1
中指数级减小的数组大小,而rec2
中只有线性减小的数组大小。