【问题标题】:Running time using Big Θ notation使用 Big Θ 表示法的运行时间
【发布时间】:2012-10-01 14:35:46
【问题描述】:
def filter(f, lst):     
    if lst == []: return []
    if f(lst[0]): return [lst[0]] + filter(f, lst[1:])
    return filter(f, lst[1:]) 

def my_reverse(lst):        # Reverse the list
    def reverse_helper(x,y):
        if x == []: return y
        return reverse_helper(x[1:], [x[0]] + y)
    return reverse_helper(lst, []) 

def revfilter_alpha(f, lst):    # Reverse and filter ...
    return my_reverse(filter(f, lst))

def revfilter_beta(f, lst): # Reverse and filter ...
    if lst == []: return []
    return revfilter_beta(f, lst[1:]) + ([lst[0]]  if f(lst[0])  else [])   

有人可以向我解释如何以 Big Θ 表示法确定这些的运行时间吗?我已经阅读了很多东西,但仍然不知道从哪里开始。

filter 中,我认为它是 Θ(n^2),因为它使用谓词函数 f 检查大小为 n 的列表中的每个元素,并进行 n 个递归调用,因此 n*n。

revfilter_beta 看起来非常相似,只是在过滤时反转,所以这不也是 Θ(n^2) 吗?

revfilter_alpha 会过滤然后反向,所以这不是 n^2*n^2 = Θ(n^4) 吗?

有人有什么想法吗?

【问题讨论】:

  • filter 中进行了多少次递归调用?真的是n * n吗?
  • 顺便说一句,filterreverse 一样是内置的,你也可以称它为my_filter
  • 现在我不确定我是否正确开始......:P
  • 注意:通常称为Big O notationΘ(n) 提供比O(n) 更严格的界限。

标签: python algorithm analysis


【解决方案1】:

filtern 递归调用,但您还要在每次迭代中执行一个复制操作,该操作采用 n,因此您最终得到 Θ(n^2)。如果你“正确”实现它,它应该是 Θ(n)。

my_reverse 也一样。

revfilter_beta 也一样。

revfilter_alpha 只执行filter,然后执行reverse,因此 Θ(n^2 + n^2) = Θ(n^2)。


编辑:让我们再看看filter

您要计算的是相对于输入的大小执行了多少操作。 O(n) 意味着在最坏的情况下,您将按照n 的顺序进行操作。我说“按顺序”是因为您可以执行O(n/2) 操作或O(4n),但最重要的因素是n。也就是说,随着n的增长,常数因素变得越来越不重要,所以我们只看非常数因素(本例中为n)。

那么,filter 在大小为 n 的列表上执行了多少操作?

让我们从下往上看。如果 n 为 0 - 一个空列表怎么办?然后它只会返回一个空列表。假设这是 1 次操作。

如果n 为 1 会怎样?它将检查是否应该包含lst[0] - 无论调用f 需要多长时间,它都会检查 - 然后它将复制列表的其余部分,并对该副本进行递归调用,在这种情况下是空列表。所以filter(1) 需要f + copy(0) + filter(0) 操作,其中copy(n) 是复制列表所需的时间,f 是检查是否应包含元素所需的时间,假设它需要相同的时间每个元素。

filter(2) 呢?它将执行 1 次检查,然后复制列表的其余部分并在其余部分上调用 filterf + copy(1) + filter(1)

你已经可以看到模式了。 filter(n) 接受 1 + copy(n-1) + filter(n-1)

现在,copy(n) 只是 n - 它需要 n 操作才能以这种方式对列表进行切片。所以我们可以进一步简化:filter(n) = f + n-1 + filter(n-1)

现在您可以尝试扩展filter(n-1) 几次,看看会发生什么:

filter(n) = f + n-1 + filter(n-1)
          = 1 + n-1 + (f + n-2 + filter(n-2))
          = f + n-1 + f + n-2 + filter(n-2)
          = 2f + 2n-3 + filter(n-2)
          = 2f + 2n-3 + (f + n-3 + filter(n-3))
          = 3f + 3n-6 + filter(n-3)
          = 3f + 3n-6 + (f + n-4 + filter(n-4))
          = 4f + 4n-10 + filter(n-4)
          = 5f + 5n-15 + filter(n-5)
          ...

我们可以概括x 重复吗?那个1, 3, 6, 10, 15...序列就是三角形数——即11+21+2+31+2+3+4等。从1x的所有数字的总和是@987654366 @。

          = x*f + x*n - x*(x-1)/2 + filter(n-x)

现在,x 是什么?我们会有多少次重复?好吧,您可以看到当x = n 时,您不再有递归-filter(n-n)=filter(0)=1。所以我们现在的公式是:

filter(n) = n*f + n*n - n*(n-1)/2 + 1

我们可以进一步简化:

filter(n) = n*f + n^2 - (n^2 - n)/2 + 1
          = n*f + n^2 - n^2/2 + n/2 + 1
          = n^2 - n^2/2 + f*n + n/2 + 1
          = (1/2)n^2 + (f + 1/2)n + 1

所以你有它 - 一个相当详细的分析。那将是 Θ((1/2)n^2 + (f + 1/2)n + 1)... 假设 f 是微不足道的(比如 f=1),它会到达 Θ((1/2)n^2 + (3/2)n + 1)

现在您会注意到,如果 copy(n) 花费了恒定的时间而不是线性时间(如果 copy(n) 是 1 而不是 n),那么您将不会得到 n^2在那里术语。

我承认,当我最初说Θ(n^2) 时,我并没有在脑海中做这一切。相反,我想:好的,你有n 递归步骤,每个步骤都将花费n 的时间,因为copyn*n = n^2,因此Θ(n^2)。为了更准确地做到这一点,n 在每一步都会缩小,所以你真的有n + (n-1) + (n-2) + (n-3) + ... + 1,它最终与上面的数字相同:n*n - (1 + 2 + 3 + ... + n) = n*n - n*(n-1)/2 = (1/2)n^2 + (1/2)n,这是相同的如果我在上面使用了0 而不是f。同样,如果您有n 步骤但每个步骤都采用1 而不是n(如果您不必复制列表),那么您将有1 + 1 + 1 + ... + 1n 次,或者只是n

但是,这需要更多的直觉,所以我想我也会向您展示可以应用于任何事物的蛮力方法。

【讨论】:

  • 等等,它们都是Θ(n^2)?您能否更详细地向我解释您的推理以及如何解决这些问题?我仍然感到很失落。
  • @TidusSmith: 好了……这有什么意义吗?呵呵
  • 确实如此!谢谢!我的教授用大约 2 句话解释了它,所以我想我只是感到困惑,因为它看起来还有很多内容哈哈。
【解决方案2】:

您的所有函数都是O(N^2),因为它们在每个递归步骤中花费O(N) 时间,并且在长度为N 的列表中会有N 步骤。

您在函数中执行了两个昂贵的(即O(N))操作。第一个是切片(例如lst[1:])。第二个是列表连接(使用+ 运算符)。

这两者都可能比您预期的要贵,主要是因为 Python 的列表与其他语言中的列表数据类型不同。在引擎盖下它们是数组,而不是链表。可以在 O(1) 时间内对链表执行上述操作(尽管O(1) 切片具有破坏性)。例如,在 Lisp 中,您使用的算法将是 O(N),而不是 O(N^2)

递归在 Python 中也经常是次优的,因为没有 tail call elimination。在最近的版本中,Python 的默认递归限制是 1000,所以长列表会破坏纯粹的递归解决方案,除非你在 sys 模块中乱来增加限制。

也可以在 Python 中执行这些算法的O(N) 版本,但您需要尽可能避免上述昂贵的列表操作。我建议不要使用递归,而是使用生成器,这是一种更“pythonic”的编程风格。

使用生成器进行过滤非常容易。内置的filter 函数已经完成了,但您只需几行代码即可编写自己的函数:

def my_filter(f, iterable):
    for e in iterable:
        if f(e):
            yield e

颠倒顺序有点复杂,因为您需要能够对源进行随机访问或使用O(N) 额外空间(您的算法使用该空间的堆栈,即使列表遵循序列协议,可以随机访问)。内置的reversed 函数仅适用于序列,但这里有一个适用于任何可迭代对象(例如另一个生成器)的版本:

def my_reversed(iterable):
    storage = list(iterable)  # consumes all the input!
    for i in range(len(storage)-1, -1, -1):
        yield storage[i]

请注意,与许多生成器不同,this on 在开始产生输出之前会立即消耗其所有输入。不要在无限输入上运行它!

您可以按任意顺序组合它们,my_reversed(filter(f, lst)) 应该等同于 filter(f, my_reversed(lst))(尽管对于后者,使用内置的 reversed 函数可能更好)。

上述两个生成器的运行时间(以及它们的组成顺序)将为O(N)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多