以下示例摘自Cracking the coding interview(第6版)一书。根据本书,以下代码的时间复杂度为O(n ^ 2 * n!)。 (请参阅示例12.第32,33页)
public static void main(String[] args) {
new PermutationsTest().permutations("ABCD");
}
void permutations(String string) {
permutations(string, "");
}
static int methodCallCount = 0;
void permutations(String string, String perfix) {
if (string.length() == 0) {
System.out.println(perfix);
} else {
for (int i = 0; i < string.length(); i++) {
String rem = string.substring(0, i) + string.substring(i + 1);
permutations(rem, perfix + string.charAt(i));
}
}
System.out.format("Method call count %s \n", methodCallCount);
methodCallCount += 1;
}
我发现很难理解它的计算方法。以下是我的想法。
可以有n!安排。所以应该至少有n!调用。但是,对于每次通话,大约需要n次工作。 (因为它需要遍历传递的字符串)。所以答案不应该是O(n * n!)?
但真正发生的是每次调用都需要为(n-1)个字符串完成循环。所以我们可以说它应该是n! * n(n + 1)/ 2
请解释..
答案 0 :(得分:2)
有n!
个可能的字符串,但添加到字符串的每个字符都需要:
String rem = string.substring(0, i) + string.substring(i + 1);
permutations(rem, perfix + string.charAt(i));
substring
次调用和字符串连接为O(n)
。对于字符串中的每个字符O(n^2)
,所有字符串都为O(n^2 * n!)
答案 1 :(得分:1)
要获得渐近时间复杂度,您需要计算调用permutations
函数的时间以及它的渐近时间复杂度。答案就是这些的产物。
string.length() = len
每次迭代时总是减少1,因此1
需要len=n
,len=n-1
需要n*(n-1)
,len = n-2
{ {1}},...,n!
调用len = 0.因此,调用总数为:
n!/1! + n!/2! + n!/3! + n!/4! + .. + n!/n! = sum(k=1..n) n!/k!
在渐近极限中,可以计算:
sum(k=1..n)( n!/k! ) = n! (-1 + sum(k=0..n) 1/k! (1^k)) -> n! (e^1 - 1) = (e-1)*n!,
是O((1-e)* n!)= O(n!)。 e
是Napier constant 2.71828128 ..要计算我在e^x = sum(k=0..infinity) 1/k! x^k
使用泰勒系列x=1
的总和。
现在,对于函数的每次调用,都有子串和连接操作:
String rem = string.substring(0, i) + string.substring(i + 1);
此操作需要string.length
操作的顺序,String class
需要将每个字符复制到新的字符串(String.length - 1
个操作数)。因此,总复杂度是这两个O(n*n!)
的乘积。
为了检查perm的调用是否像我说的那样,我为一个排列编写了一个小的c++
代码(没有字符串操作,所以它应该是O(n!))`。
#include <iostream>
#include <string>
#include <iomanip>
unsigned long long int permutations = 0;
unsigned long long int calls = 0;
void perm(std::string& str, size_t index){
++calls;
if (index == str.size()-1){
++permutations;
// std::cout << count << " " << str << std::endl;
}
else{
for (size_t i=index; i < str.size();++i){
std::swap(str[index],str[i]);
perm(str,index+1);
std::swap(str[index],str[i]);
}
}
}
int main(){
std::string alpha="abcdefghijklmnopqrstuvwxyz";
std::cout << std::setprecision(10);
for (size_t i=1;i<alpha.size()+1;++i){
permutations = calls = 0;
std::string str(alpha.begin(),alpha.begin()+i);
perm(str,0);
std::cout << i << " " << permutations << " " << calls << " " << static_cast<double>(calls)/permutations << std::endl;
}
}
输出:
1 1 1 1
2 2 3 1.5
3 6 10 1.666666667
4 24 41 1.708333333
5 120 206 1.716666667
6 720 1237 1.718055556
7 5040 8660 1.718253968
8 40320 69281 1.71827877
9 362880 623530 1.718281526
10 3628800 6235301 1.718281801
11 39916800 68588312 1.718281826
12 479001600 823059745 1.718281828
13 6227020800 10699776686 1.718281828
14 took too long
列为:length of the string = n
,n!
,sum(k=1..n) n!/k!
,第三列和第二列的比率,应为(e-1)=1.71828182845905
。所以它似乎会快速收敛到渐近极限。
答案 2 :(得分:1)
恐怕这本书弄错了。时间复杂度为 ϴ(n!n)
,正如在 fgb 的 answer 中正确推测的那样。
原因如下:
和递归函数一样,我们首先写下递归关系。在这种情况下,我们必须输入 string
和 perfix
[原文如此!]。让我们分别用 s
和 p
表示它们的长度:
T(0,p) = p // println
T(s,p) = s * // for (int i = 0; i < string.length(); i++)
(O(s + // String rem = string.substring(0, i) + string.substring(i + 1);
p) + // perfix + string.charAt(i)
T(s-1,p+1)) // permutations(rem, perfix + string.charAt(i));
= s*T(s-1,p+1) + O(s(s+p))
但是,请注意
s+p
始终保持不变,即 k
,即字符串 string
的原始长度。s
倒计时到 0 时,p
的长度也为 k
。因此对于特定的 k
,我们可以像这样重写递归关系:
T_k(0) = k
T_k(s) = s*T(s-1) + O(ks)
要记住的一个好规则是形式的递推关系
T(n) = n * T(n-1) + f(n)
有通用的解决方案
T(n) = n! (T(0) + Sum { f(i)/i!, for i=1..n })
在此处应用此规则会产生准确的解决方案
T_k(s) = s! (k + Sum { ki/i!, for i=1..s })
= s!k (1 + Sum { 1/(i-1)!, for i=1..s })
现在回想一下,k
是字符串 string
的原始长度,所以我们实际上只对案例 k = s
感兴趣,因此我们可以写出最终的精确解决方案案例为
T(s) = s!s (1 + Sum { 1/(i-1)!, for i=1..s })
由于级数 Sum { 1/(i-1)!, for i=1..infinity }
收敛,我们终于有
T(n) = ϴ(n!n), qed