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),因为它使用带有n个递归调用的谓词函数f检查大小为n的列表中的每个元素,因此n * n。
revfilter_beta
看起来非常相似,只是在过滤时反转,所以这也不是Θ(n ^ 2)?
revfilter_alpha
会过滤一个反向,所以这不是n ^ 2 * n ^ 2 =Θ(n ^ 4)?
有没有人有任何想法?
答案 0 :(得分:5)
filter
有n
递归调用,但你也会在每次迭代时执行一次复制操作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次检查,然后复制剩余的列表并在余数上调用filter
:f + 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
...序列是三角形数字 - 即1
,1+2
,1+2+3
,1+2+3+4
等。所有数字的总和来自1
至x
为x*(x-1)/2
。
= 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
,每个步骤都需要copy
个时间。 n*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 + ... + 1
(如果您不必复制列表),那么您将n
,n
次,或只是{{1}}。
但是,这需要更多的直觉,所以我想我也会向你展示你可以应用于任何东西的蛮力方法。
答案 1 :(得分: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]
请注意,与许多生成器不同,它会在开始产生输出之前立即消耗所有输入。不要在无限输入上运行它!
您可以按任意顺序组合这些,my_reversed(filter(f, lst))
应该等同于filter(f, my_reversed(lst))
(尽管对于后者,使用内置的reversed
函数可能更好)。
上述两个生成器的运行时间(以及它们在任何顺序中的组成)将为O(N)
。