昨天我为列表编写了两个可能的反向函数,以演示一些不同的列表反转方法。但后来我注意到使用分支递归(rev2
)的函数实际上比使用线性递归(rev1
)的函数更快,即使分支函数需要更多的调用来完成和相同的调用次数(减1)非线性调用(实际上更多的计算密集型)比线性递归函数的非平凡调用。我没有明确地触发并行性,那么性能差异来自何处使得具有更多涉及的更多调用的函数花费更少的时间?
from sys import argv
from time import time
nontrivial_rev1_call = 0 # counts number of calls involving concatentation, indexing and slicing
nontrivial_rev2_call = 0 # counts number of calls involving concatentation, len-call, division and sclicing
length = int(argv[1])
def rev1(l):
global nontrivial_rev1_call
if l == []:
return []
nontrivial_rev1_call += 1
return rev1(l[1:])+[l[0]]
def rev2(l):
global nontrivial_rev2_call
if l == []:
return []
elif len(l) == 1:
return l
nontrivial_rev2_call += 1
return rev2(l[len(l)//2:]) + rev2(l[:len(l)//2])
lrev1 = rev1(list(range(length)))
print ('Calls to rev1 for a list of length {}: {}'.format(length, nontrivial_rev1_call))
lrev2 = rev2(list(range(length)))
print ('Calls to rev2 for a list of length {}: {}'.format(length, nontrivial_rev2_call))
print()
l = list(range(length))
start = time()
for i in range(1000):
lrev1 = rev1(l)
end = time()
print ("Average time taken for 1000 passes on a list of length {} with rev1: {} ms".format(length, (end-start)/1000*1000))
start = time()
for i in range(1000):
lrev2 = rev2(l)
end = time()
print ("Average time taken for 1000 passes on a list of length {} with rev2: {} ms".format(length, (end-start)/1000*1000))
示例电话:
$ python reverse.py 996 calls to rev1 for a list of length 996: 996 calls to rev2 for a list of length 996: 995 Average time taken for 1000 passes on a list of length 996 with rev1: 7.90629506111145 ms Average time taken for 1000 passes on a list of length 996 with rev2: 1.3290061950683594 ms
答案 0 :(得分:6)
简短回答:这里的电话不是那么多,但它是复制列表的数量。因此,线性递归具有时间复杂度 O(n 2 ) ,而分支递归< / strong>时间复杂度 O(n log n) 。
此处的递归调用 not 在恒定时间内运行:它在复制列表的长度中运行。实际上,如果您复制 n 元素列表,则需要 O(n)时间。
现在,如果我们执行线性递归,则意味着我们将执行 O(n)调用(最大调用深度为 O(n))。每次,我们将完全复制列表,除了一个项目。所以时间的复杂性是:
n
---
\ n * (n+1)
/ k = -----------
--- 2
k=1
因此,算法的时间复杂度是 - 假设调用本身是在 O(1) - O(n 2 )中完成的
如果我们执行分支递归,我们制作列表的两个副本,每个副本的长度大约是一半。因此递归的每个级别将花费 O(n)时间(因为这些一半也会产生列表的副本,如果我们总结这些,我们会做一个完整的在每个递归级别复制)。但级别数按日志缩放:
log n
-----
\
/ n = n log n
-----
k=1
所以时间复杂度在这里 O(n log n)(这里 log 是2-log,但这对于大而言并不重要哦)。
我们可以使用 views 来代替复制列表:这里我们保留对相同列表的引用,但是使用两个指定列表范围的整数。例如:
def rev1(l, frm, to):
global nontrivial_rev1_call
if frm >= to:
return []
nontrivial_rev1_call += 1
result = rev1(l, frm+1, to)
result.append(l[frm])
return result
def rev2(l, frm, to):
global nontrivial_rev2_call
if frm >= to:
return []
elif to-frm == 1:
return l[frm]
nontrivial_rev2_call += 1
mid = (frm+to)//2
return rev2(l, mid, to) + rev2(l, frm, mid)
如果我们现在运行timeit
模块,我们将获得:
>>> timeit.timeit(partial(rev1, list(range(966)), 0, 966), number=10000)
2.176353386021219
>>> timeit.timeit(partial(rev2, list(range(966)), 0, 966), number=10000)
3.7402000919682905
这是因为我们不再复制列表,因此append(..)
函数在 O(1) 摊销成本中有效。对于分支递归,我们附加两个列表,因此它在 O(k)中工作,其中 k 是两个列表长度的总和。所以现在我们将 O(n)(线性递归)与 O(n log n)(分支递归)进行比较。