递归算法的时间复杂度分析

时间:2015-03-14 10:14:58

标签: algorithm time-complexity

我想知道以下算法的复杂性,最重要的是,逐步推导出它的过程。

我怀疑它是O(长度(文本)^ 2 *长度(模式))但我无法解决递归方程。

在递归调用上进行memoization(即动态编程)时,复杂性会如何提高?

此外,我希望能够帮助我学习如何分析这种算法的技巧/书籍指南。

在Python中:

def count_matches(text, pattern):
  if len(pattern) == 0: return 1

  result = 0
  for i in xrange(len(text)):
    if (text[i] == pattern[0]):
      # repeat the operation with the remaining string a pattern
      result += count_matches(text[i+1:], pattern[1:])

  return result

在C:

int count_matches(const char text[],    int text_size, 
                  const char pattern[], int pattern_size) {

  if (pattern_size == 0) return 1;

  int result = 0;

  for (int i = 0; i < text_size; i++) {
    if (text[i] == pattern[0])
      /* repeat the operation with the remaining string a pattern */
      result += count_matches(text+i, text_size-(i+1), 
                              pattern+i, pattern_size-(i+1));
  }

  return result;  
}

注意:算法故意重复每个子字符串的匹配。请不要关注算法执行的匹配,只关注其复杂性。

为算法中的(现已修复)拼写错误道歉

5 个答案:

答案 0 :(得分:2)

我的直觉是复杂度为O(长度(文本)^ 3)是不正确的。它实际上是O(n!),纯粹是因为实现的形式是

def do_something(relevant_length):
    # base case

    for i in range(relevant_length):
        # some constant time work

        do_something(relevant_length - 1)

Example of O(n!)?

中讨论过

如果使用memoization,则会生成一次递归树,然后每次都会查找。

描绘递归树的形状。

我们每层进行一个字符的进度。有2个基本案例。当我们到达模式结束时,如果文本中不再有任何要迭代的字符,则递归最低点。第一个基本情况是显式的,但第二个基本情况恰好在实现时发生。

因此递归树的深度(高度)是min [length(text),length(pattern)]。

有多少子问题?我们还每层进行一个字符的进度。如果比较文本中的所有字符,使用高斯技巧求和S = [n(n + 1)] / 2,将在所有递归层中评估的子问题总数为{length(text)* [篇(文字)+ 1]} / 2。

取长度(文本)= 6和长度(模式)= 10,其中长度(文本)&lt;长度(图案)。深度为min [长度(文本),长度(模式)] = 6.

PTTTTT
PTTTT
PTTT
PTT
PT
P

如果长度(文本)= 10且长度(模式)= 6,则长度(文本)&gt;长度(图案)。深度为min [长度(文本),长度(模式)] = 6.

PTTTTTTTTT
PTTTTTTTT
PTTTTTTT
PTTTTTT
PTTTTT
PTTTT

我们看到的是长度(模式)并不真正有助于复杂性分析。在长度(模式)<1的情况下。长度(文字),我们只是扼杀了一点高斯和。

但是,由于文本和模式一对一地向前迈进,我们最终做的工作少得多。递归树看起来像方阵的对角线。

对于length(text)= 6和length(pattern)= 10以及length(text)= 10和length(pattern)= 6,树是

P
 P
  P
   P
    P
     P

因此,记忆方法的复杂性是

O(min(长度(文本),长度(模式)))

编辑:给@fons注释,如果从未触发递归怎么办?特别是在所有i的text [i] == pattern [0]永远不变的情况下。然后迭代所有文本是主导因素,即使长度(文本)&gt;长度(图案)。

这意味着记忆方法的实际上限是

O(最大(长度(文本),长度(模式)))

在长度(文本)&gt;的情况下考虑更多一点。即使模式耗尽,也会触发长度(模式)和递归IS,它需要恒定的时间来递归并检查模式现在是否为空,因此长度(文本)仍占主导地位。

这使得te记忆版本O的上限(长度(文本))。

答案 1 :(得分:0)

嗯...我可能错了,但据我所知,你的运行时应该专注于这个循环:

for c in text:
    if (c == pattern[0]):
      # repeat the operation with the remaining string a pattern
      result += count_matches(text[1:], pattern[1:])

基本上让文字的长度为 n ,我们不需要模式的长度。

第一次运行此循环(在父函数中),我们将对其进行 n 调用。在最糟糕的情况下,每次 n 调用都会调用程序的 n-1 个实例。然后,那些 n-1 实例将在最坏的情况下调用 n-2 实例,依此类推。

这导致一个方程式 n *(n-1)(n-2) ... * 1 ,这是 n !即可。所以最糟糕的情况是 O(n!)。非常糟糕(:

我使用输入运行你的python程序几次会导致最糟糕的运行时:

  

在[21]中:count_matches(&#34; aaaaaaa&#34;,&#34; aaaaaaa&#34;)

     

Out [21]:5040

     

在[22]中:count_matches(&#34; aaaaaaaa&#34;,&#34; aaaaaaaa&#34;)

     

Out [22]:40320

     

在[23]中:count_matches(&#34; aaaaaaaaa&#34;,&#34; aaaaaaaaa&#34;)

     

出[23]:362880

最后一个输入是9个符号和9个! = 362880。

要分析算法的运行时间,首先需要考虑导致最差可能运行时的输入。在您的算法中,最佳和最差的变化相当大,因此您可能需要进行平均案例分析,但这非常复杂。 (您需要定义哪些输入是平均值,以及最坏情况的常见程度。)

动态编程可以帮助缓解运行时间,但分析更难。让我们的第一个代码成为一个简单的未经优化的动态编程版本:

cache = {}
def count_matches_dyn(text, pattern):
  if len(pattern) == 0: return 1

  result = 0
  for c in text:
    if (c == pattern[0]):
      # repeat the operation with the remaining string a pattern
      if ((text[1:], pattern[1:]) not in cache.keys()):
        cache[(text[1:], pattern[1:])] = count_matches_dyn(text[1:], pattern[1:])
        result += cache[(text[1:], pattern[1:])]
      else:
        result += cache[(text[1:], pattern[1:])]

  return result

这里我们缓存对字典中count_matches的所有调用,所以当我们用相同的输入调用count匹配时,我们将得到结果,而不是再次调用该函数。 (这被称为memoization)。

现在让我们分析一下。主循环

  for c in text:
    if (c == pattern[0]):
      # repeat the operation with the remaining string a pattern
      if ((text[1:], pattern[1:]) not in cache.keys()):
        cache[(text[1:], pattern[1:])] = count_matches_dyn(text[1:], pattern[1:])
        result += cache[(text[1:], pattern[1:])]
      else:
        result += cache[(text[1:], pattern[1:])]

第一次调用时会运行 n 次(我们的缓存为空)。但是,第一个递归调用将填充缓存:

cache[(text[1:], pattern[1:])] = count_matches_dyn(text[1:], pattern[1:])

同一循环中的每个其他调用都会花费(O(1)。所以基本上顶级递归将花费 O(n-1)+(n-1)* O(1)= O(n-1)+ O(n-1)= 2 * O(n-1)。你可以看到,从递归下来的调用中只有第一个将下降许多递归调用( O(n-1)调用),其余的将花费 O(1),因为它们只是字典查找。鉴于所有这些都说运行时(2 * O(n-1),摊销为 O(n)

声明。我不完全确定动态编程版本的分析,请随时纠正我(:

免责声明2.动态编程代码包含昂贵的操作(文本[1:],模式[1:]),这些操作在分析中不作为因素。这是有目的的,因为在任何合理的实现中,您可以大大降低这些调用的成本。重点是展示简单缓存如何大幅减少运行时间。

答案 2 :(得分:0)

  • 首先,让我们超越代码并制定此代码试图解决的问题。

Python版本似乎将pattern的出现次数计算为text的{​​{3}}。 C版本目前看起来很糟糕,所以我假设P​​ython版本是正确的。

  • 然后,回顾一下代码,并注意一些关于如何执行解决方案的一般事项。

该函数通过将0和1相加来计算答案。因此,操作的数量至少是为了得到答案而需要加起来的1的数量,即答案本身。

  • 现在,让我们设计一个输入(text, pattern),它将为给定长度的textpattern提供最差的运行时间。

最大的答案显然是某些字母相等的情况。

  • 之后,我们使用上面的输入简化和一些数学知识来直接计算答案。

当所有字母相等时,答案基本上是从k = len (pattern)中选择n = len (text)项(字母)的方式的数量,即subsequence

  • 接下来,我们选择textpattern的长度,这会给我们带来最糟糕的复杂性。

例如:对于text = 'a' * 100pattern = 'a' * 50,我们得到答案choose (100, 50) = 100! / 50! / 50!。 通常,对于text的固定长度,pattern的长度必须是其中的一半,如有必要,可以在任意一侧舍入。 这是看到choose (n, k)时得到的直观概念。 形式上,通过手工比较choose (n, k)choose (n, k+-1)来证明这一点很简单。

  • 估计我们得到的答案。

总和choose (n, 0) + choose (n, 1) + ... + choose (n, n)是2 n ,再次直观地说,choose (n, n/2)是相当一部分。 更正式的是,Pascal's triangle Stirling's formula choose (n, n/2)大约为2 n 除以sqrt(n)

  • 最后,请注意可能不需要更详细的分析。

当复杂度呈指数时,我们通常对精确多项式因子不太感兴趣。 比方说,2 100 O (2^n))和100次2 100 O (n * 2^n))操作同样不可能在合理的时间内完成。 重要的是将O (2^n)减少到O (2^(n/2)),或者更好,以找到多项式解。

  • 回想一下,我们发现的是下限。

实际上,如果我们在顶部添加以下行,那么复杂性确实会choose (len (text), len (pattern)乘以某个多项式:

if len(pattern) < len(text): return 0

实际上,如果文本中剩余的字母数小于模式的长度,则无法匹配。

  • 以下是另一个角度的视图。

否则,我们可以有更多的递归分支,最终导致在答案中加0。

从另一方面来看,我们可以证明未更改代码中的操作数量可以高达len(text)的幂。

确实,在text = 'a' * npattern = 'a' * n时,假设我们已经处理了k的{​​{1}}个字母。 这些字母中的每一个都独立于其他字母,可以与text的某个字母匹配,也可以在循环中省略。 因此,对于pattern的每个字母,我们有两种方法可供使用,因此当我们处理text 2^n的{​​{1}}个字母时,我们有n个方法,即到达终止我们递归函数的调用。

答案 3 :(得分:0)

时间复杂度应该提高到某种程度 O(长度(文本)*长度(模式))来自递归的(O(n!))。

记忆解决方案(DP)将涉及构建text-vs-pattern的查找表,可以从文本和模式的末尾开始逐步建立。

答案 4 :(得分:-1)

我担心你的算法对于模式匹配是不正确的。 主要是因为一旦发现第一个字符匹配,它将在文本的其余部分中搜索子子字符串。 例如,对于文本“abbccc”和模式“accc”,您的算法将返回等于1的结果。

您应该考虑为模式匹配实现“朴素”算法,这与您尝试的非常相似,但没有递归。 其复杂度为O(n * m),其中'n'是文本长度,'m'是模式长度。 在Python中,您可以使用以下实现:

text = "aaaaabbbbcccccaaabbbcccc"
pattern = "aabb"
result = 0

    index = text.find(pattern)
    while index > -1:
        result += 1
        print index
        index = text.find(pattern, index+1)

return result

关于这个主题的书籍,我最好的建议是Cormen的"Introduction to Algorithms",它涵盖了算法和复杂性的所有材料。