这个算法的运行时间是什么?

时间:2013-05-20 01:49:11

标签: c algorithm complexity-theory

我刚刚写了一个问题的答案:

longest common subsequence: why is this wrong?

这个函数应该找到两个字符串之间最长的子字符串,但是当我试图找出最坏情况的运行时和导致它的输入时,我意识到我不知道。将代码视为C伪代码。

// assume the shorter string is passed in as A
int lcs(char * A, char * B)
{
  int length_a = strlen(A);
  int length_b = strlen(B);

  // This holds the length of the longest common substring found so far
  int longest_length_found = 0;

  // for each character in one string (doesn't matter which), look for 
  //   incrementally larger strings in the other
  // once a longer substring can no longer be found, stop
  for (int a_index = 0; a_index < length_a - longest_length_found; a_index++) {
    for (int b_index = 0; b_index < length_b - longest_length_found; b_index++) {

      // check the next letter until a mismatch is found or one of the strings ends.
      for (int offset = 0; 
           A[a_index+offset] != '\0' && 
             B[b_index+offset] != '\0' && 
             A[a_index+offset] == B[b_index+offset]; 
           offset++) {          
        longest_length_found = longest_length_found > offset ? longest_length_found : offset;
      }
    }
  }
  return longest_found_length;
}

到目前为止,这是我的想法:

下面,我假设A和B的大小大致相同,不必说A B A,我只会说n ^ 3。如果这非常糟糕,我可以更新问题。

如果代码中没有一些优化,我相信N ^ 3运行时的运行时是A B A.

但是,如果字符串不相似并且永远找不到长子字符串,那么最内层的for循环将退出到一个常数,留下A * B,对吗?

如果字符串完全相同,则算法需要线性时间,因为每个字符串只有一个同时传递。

如果字符串相似但不相同,那么longest_length_found将成为A或B中较小者的重要部分,这将分解N ^ 3中的一个因子,留下我们N ^ 2,右? 我只是想了解当它们非常相似但不完全相同时会发生什么。

大声思考,如果在第一个字母上,你会发现一个长度约为A长度一半的子串。这意味着你将运行第一个循环的A / 2次迭代,B-(A / 2)迭代第二个循环,然后在第三个循环中进行A / 2次迭代(假设字符串非常相似),而没有找到更长的子字符串。假设大致均匀的长度字符串,则为N / 2 * N / 2 * N / 2 = O(N ^ 3)。

可能显示此行为的示例字符串:

A A A B A A A B A A A B A A A B

A A A A B A A A A B A A A A B A

我是关闭还是我遗漏了某些东西或误用了某些东西?

我很确定我可以使用trie /前缀树做得更好,但同样,我真的很想了解这个特定代码的行为。

2 个答案:

答案 0 :(得分:1)

我认为评论中所说的 roliu 是对钱的抨击。我认为你的算法是 O(N 3 ,最好的情况是 O(N 2

我实际想要指出的是这种算法的过度放纵。您会看到,对于每个字符串中的每个可能的起始偏移量,您将测试每个后续匹配字符以计算匹配数。但请考虑这样的事情:

A = "01111111"
B = "11111110"

您将发现的第一件事是从A[1]B[0]开始的最大匹配子字符串,然后您将测试从A[2]开始的那些完全重叠的部分, B[1]等等......这里重要的是相对偏移量。通过实现这一点,您可以完全删除算法的 N 3 部分。然后它变成移动其中一个阵列在另一个下面的问题。

A         01111111
B  11111110
B   11111110
B    11111110
B        ... -->
B                11111110

为了使代码不那么复杂,你可以测试一半的系统,然后交换数组并测试另一半:

// Shift B under A
A  01111111
B  11111110
B      ... -->
B         11111110

// Shift A under B
B  11111110
A  01111111
A      ... -->
A         01111111

如果你这样做,那么你就有 O((A + B-2)* min(A,B)/ 2),或更方便 O(N < SUP> 2 )

int lcs_half(char * A, char * B)
{
    int maxlen = 0, len = 0;
    int offset, i;
    for( offset = 0; B[offset]; offset++ )
    {
        len = 0;
        for( i = 0; A[i] && B[i+offset]; i++ )
        {
            if( A[i] == B[i+offset] ) {
                len++;
                if( len > maxlen ) maxlen = len;
            }
            else len = 0;
        }
    }
    return maxlen;
}

int lcs(char * A, char * B)
{
    int run1 = lcs_half(A,B);
    int run2 = lcs_half(B,A);
    return run1 > run2 ? run1 : run2;
}

答案 1 :(得分:1)

因此,在我们在评论中讨论它之后,我们同意问题是找到代码的最坏情况运行时。我们可以使用以下证明至少Omega(n^3)声明它:


A = aaaa...aabb...bbbb表示|A| = n,它由n/2 a&{39}和n/2 b&#39;组成。秒。
B = aaaa....其中|B| = n

现在我们考虑最外层循环的第一次n/2次迭代(即n/2字符串的第一个A起始索引。修复最外层循环的第一次i次迭代的一些迭代n/2。第二个循环的上限是至少 n-n/2 = n/2,因为两个字符串的LCS长度为n/2。对于第二个循环的每次迭代,我们匹配一个长度为n/2 - i的字符串(您可以通过矛盾证明这一点)。所以我们在最外层循环的第一次n/2次迭代之后得到了这一行:

longest_length_found = longest_length_found > offset ? longest_length_found : offset;

已经运行:

n/2*(n/2) + n/2*(n/2-1) + n/2*(n/2-2) + ... + n/2*(2) + n/2*(1) = n/2*Omega(n^2) = Omega(n^3)

具体来说,对于最外层循环的第一次迭代,我们在字符串n/2中有a A&#39;并且n/2 B中的起始点。对于B中的每个起始点,我们将匹配长度为n/2的完整公共子字符串(意味着我们将点击该行n/2次)。这就是n/2*(n/2)。对于最外层循环的下一次迭代,我们在字符串n/2-1中有一个a A字符串,n/2中仍有B个起始点{1}}。在这种情况下,我们为每个起始索引n/2-1匹配长度为=> n/2(n/2-1)的公共子字符串。同样的论证归纳为i = n/2

无论如何,我们知道算法在输入上的运行时间比最外层循环的第一次n/2次迭代的运行时间长,所以它也是Omega(n^3)