字符串排列的时间复杂度

时间:2017-02-06 12:36:14

标签: algorithm time-complexity big-o

以下示例摘自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

请解释..

3 个答案:

答案 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=nlen=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!)。 eNapier 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 = nn!sum(k=1..n) n!/k!,第三列和第二列的比率,应为(e-1)=1.71828182845905。所以它似乎会快速收敛到渐近极限。

答案 2 :(得分:1)

恐怕这本书弄错了。时间复杂度为 ϴ(n!n),正如在 fgb 的 answer 中正确推测的那样。

原因如下:

和递归函数一样,我们首先写下递归关系。在这种情况下,我们必须输入 stringperfix [原文如此!]。让我们分别用 sp 表示它们的长度:

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))

但是,请注意

  1. s+p 始终保持不变,即 k,即字符串 string 的原始长度。
  2. 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