簡體   English   中英

為什么分支遞歸比線性遞歸更快(例如:列表反轉)

[英]Why is branching recursion faster than linear recursion (example: list inversion)

昨天我為列表編寫了兩個可能的反向函數,以演示一些不同的列表反轉方法。 但后來我發現,使用分支遞歸(功能rev2 )實際上比使用線性遞歸(函數更快rev1 ),即使分支功能需要更多的調用來完成和相同數量的調用(減一)的非平凡調用(實際上更多的計算密集)比線性遞歸函數的非平凡調用。 我沒有明確地觸發並行性,那么性能差異來自何處使得具有更多涉及的更多調用的函數花費更少的時間?

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 

簡短的回答 :這里的調用並不多,但它是復制列表的數量 結果, 線性遞歸具有時間復雜度O(n 2 ),其中分支遞歸具有時間復雜度O(n log n)

遞歸調用在這里固定的時間進行操作: 在列表中它復制的長度工作。 實際上,如果復制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,但就大哦來說無關緊要)。

使用視圖

我們可以使用視圖而不是復制列表:這里我們保持對同一列表的引用,但使用兩個指定列表范圍的整數。 例如:

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) (分支遞歸)進行比較。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM