[英]How to find the recurrence relation, and calculate Master Theorem of a Merge Sort Code?
我試圖找到這個合並排序代碼的主定理,但首先我需要找到它的遞歸關系,但我很難做到並理解兩者。 我已經在這里看到了一些類似的問題,但無法理解解釋,例如,首先我需要找出代碼有多少操作? 有人可以幫我嗎?
def mergeSort(alist):
print("Splitting ",alist)
if len(alist)>1:
mid = len(alist)//2
lefthalf = alist[:mid]
righthalf = alist[mid:]
mergeSort(lefthalf)
mergeSort(righthalf)
i=0
j=0
k=0
while i < len(lefthalf) and j < len(righthalf):
if lefthalf[i] < righthalf[j]:
alist[k]=lefthalf[i]
i=i+1
else:
alist[k]=righthalf[j]
j=j+1
k=k+1
while i < len(lefthalf):
alist[k]=lefthalf[i]
i=i+1
k=k+1
while j < len(righthalf):
alist[k]=righthalf[j]
j=j+1
k=k+1
print("Merging ",alist)
alist = [54,26,93,17,77,31,44,55,20]
mergeSort(alist)
print(alist)
好的,讓我們開始分析。 假設您的列表有n
元素,並且n
是 2 的冪(避免出現大小為(n+1)/2 and (n-1)/2
的子列表而不失一般性的問題)。 我正在跳過打印和一些具有恆定時間 ( +c'
) 的命令。
if len(alist)>1:
mid = len(alist)//2
通過遍歷所有元素,可以在線性時間內完成計數。 除以 2 不會改變整體行為:
T(n) = (a_1)*n + (a_2)*n +... + c'
lefthalf = alist[:mid]
righthalf = alist[mid:]
拆分列表可以理解為復制列表,所以線性復雜度T(n) = (a_1)*n + (a_2)*n + (a_3)*(n/2)*2 +... + c'
mergeSort(lefthalf)
mergeSort(righthalf)
這是棘手的部分。 您對mergeSort
合並排序的時間復雜度一無所知。 但是您知道調用的輸入大小: n/2
重新調用T(n) = (a_1)*n + (a_2)*n + (a_3)*n + T(n/2) + T(n/2) +... + c'
while i < len(lefthalf) and j < len(righthalf):
if lefthalf[i] < righthalf[j]:
alist[k]=lefthalf[i]
i=i+1
else:
alist[k]=righthalf[j]
j=j+1
k=k+1
這里開始一個循環,這意味着循環的每個命令都可以像循環重復一樣頻繁地執行。 值得慶幸的是,內容只是具有恆定時間的命令。 唯一困難的部分是確定最多只能為(len(lefthalf) -1) + (len(righthalf) -1)
的迭代次數,最多為n-2
(並且至少為n/2-1
)。 將再次檢查while條件,導致時間復雜度上限(a_4)*(n-2) + c_4 = (a_4)*n - 2*a_4 + c_4 = (a_4)*n + c_4'
其他兩個循環也會發生類似的事情(里面的命令是常量,最大n/2
循環,如果條件為假,則開銷恆定),上限:
(a_5)*n/2 + c_5 = (a_5')*n + c_5
和(a_6)*n/2 + c_6 = (a_6')*n + c_6
總結總結:
T(n) = (a_1)*n + (a_2)*n + (a_3)*n + T(n/2) + T(n/2) + (a_4)*n + c_4' + (a_5')*n + c_5 + (a_6')*n + c_6 + c'
= (a_1+a_2+a_3+a_4+a_5'+a_6')*n + 2T(n/2) + c_4'+c_5+c_6+c'
= 2T(n/2) + a'*n + c
這是上限 (Big O) 的公式,但計算下限 (Big Omega) 將以相同的結構結束,只有a'
和c
的實際(但無意義)值會有所不同。
所以知道你有一個公式可以使用。 大師定理可以應用於T(n) = a*T(n/b) + f(n)
。 您可以清楚地看到a=2
、 b=2
和f(n) = a'*n + c
。 現在,根據f(n)
的復雜度以及a
和b
的關系,可以將T(n)
的復雜度分為 3 種情況之一。 首先,您需要計算臨界常數c_crit = log_b a = log_2 2 = 1
。 現在我們需要f(n) = a'*n + c = O(n) = O(n^1)
的復雜度(線性復雜度)。
f(n) = O(n^c)
其中c
小於c_crit
不適用(1 不小於 1)f(n) = Omega(n^c)
其中c
大於c_crit
不適用(當 Big O 為上限時,Big Omega 為下限)f(n) = Theta(n^c_crit log^kn)
對於k>=0
: c_crit = 1
, k=0
。 這個條件成立,所以從主定理,你現在可以得出結論T(n) = Theta(n^c_crit log^(k+1) n) = Theta(n^1 log^(0+1) n) = Theta(n log(n))
所以這個算法的復雜度n log(n)
。 此示例中f(n)
的 Big O、Big Omega 和 Big Theta 相同,但在其他示例中可能不同。
要使用主定理確定分治算法的運行時間,您需要將算法的運行時間表示為輸入大小的遞歸 function,格式為:
T(n) = aT(n/b) + f(n)
T(n)
是我們如何在輸入大小 n 上表達算法的總運行時間。
a
代表算法進行的遞歸調用的次數。
T(n/b)
表示遞歸調用: n/b
表示遞歸調用的輸入大小是原始輸入大小的某個特定部分(分治法的除法部分)。
f(n)
表示您在算法主體中需要做的工作量,通常只是將遞歸調用的解決方案組合成一個整體解決方案(您可以說這是征服部分)。
下面是對mergeSort 的稍微重構的定義:
def mergeSort(arr):
if len(arr) <= 1: return # array size 1 or 0 is already sorted
# split the array in half
mid = len(arr)//2
L = arr[:mid]
R = arr[mid:]
mergeSort(L) # sort left half
mergeSort(R) # sort right half
merge(L, R, arr) # merge sorted halves
我們需要確定a
、 n/b
和f(n)
因為每次對 mergeSort 的調用都會進行兩次遞歸調用: mergeSort(L)
和mergeSort(R)
, a=2
:
T(n) = 2T(n/b) + f(n)
n/b
表示進行遞歸調用的當前輸入的比例。 因為我們要找到中點並將輸入分成兩半,將當前數組的一半傳遞給每個遞歸調用, n/b = n/2
和b=2
。 (如果每個遞歸調用得到原始數組b
的 1/4 則為4
)
T(n) = 2T(n/2) + f(n)
f(n)
表示算法除了進行遞歸調用之外所做的所有工作。 每次調用 mergeSort 時,我們都會計算 O(1) 時間的中點。 我們還將數組拆分為L
和R
,從技術上講,創建這兩個子數組副本是 O(n)。 然后,假設mergeSort(L)
對數組的左半部分進行排序,而mergeSort(R)
對右半部分進行排序,我們仍然必須將已排序的子數組合並在一起,以使用merge
function 對整個數組進行排序。 總之,這使得f(n) = O(1) + O(n) + complexity of merge
。 現在讓我們看一下merge
:
def merge(L, R, arr):
i = j = k = 0 # 3 assignments
while i < len(L) and j < len(R): # 2 comparisons
if L[i] < R[j]: # 1 comparison, 2 array idx
arr[k] = L[i] # 1 assignment, 2 array idx
i += 1 # 1 assignment
else:
arr[k] = R[j] # 1 assignment, 2 array idx
j += 1 # 1 assignment
k += 1 # 1 assignment
while i < len(L): # 1 comparison
arr[k] = L[i] # 1 assignment, 2 array idx
i += 1 # 1 assignment
k += 1 # 1 assignment
while j < len(R): # 1 comparison
arr[k] = R[j] # 1 assignment, 2 array idx
j += 1 # 1 assignment
k += 1 # 1 assignment
這個 function 有更多的事情要做,但我們只需要得到它的整體復雜性 class 就能夠准確地應用主定理。 我們可以計算每一個操作,即每一個比較、數組索引和賦值,或者更一般地對其進行推理。 一般來說,您可以說,在三個 while 循環中,我們將遍歷 L 和 R 的每個成員,並將它們分配給 output 數組 arr,為每個元素執行恆定數量的工作。 注意我們正在處理 L 和 R 的每個元素(總共 n 個元素),並且為每個元素做恆定量的工作就足以說明合並在 O(n) 中。
但是,如果您願意,您可以更具體地使用計數操作。 對於第一個 while 循環,每次迭代我們進行 3 次比較、5 個數組索引和 2 個賦值(常數),並且循環運行直到 L 和 R 之一被完全處理。 然后,接下來的兩個 while 循環之一可能會運行以處理來自另一個數組的任何剩余元素,執行 1 個比較、2 個數組索引和每個元素的 3 個變量分配(持續工作)。 因此,因為 L 和 R 的 n 個總元素中的每一個都會導致在 while 循環中執行最多恆定數量的操作(根據我的計數,10 或 6 個,所以最多 10 個),並且i=j=k=0
語句只有 3 個常量賦值,合並在 O(3 + 10*n) = O(n) 中。 回到整體問題,這意味着:
f(n) = O(1) + O(n) + complexity of merge
= O(1) + O(n) + O(n)
= O(2n + 1)
= O(n)
T(n) = 2T(n/2) + n
在我們應用主定理之前的最后一步:我們希望 f(n) 寫成 n^c。 對於 f(n) = n = n^1, c=1
。 (注意:如果 f(n) = n^c*log^k(n) 而不是簡單的 n^c,情況會發生非常輕微的變化,但我們在這里不必擔心)
您現在可以應用主定理,它最基本的形式是比較a
(遞歸調用的數量增長的速度)與b^c
(每個遞歸調用的工作量減少的速度)。 有 3 種可能的情況,我試圖解釋其中的邏輯,但如果括號中的解釋沒有幫助,你可以忽略它們:
a > b^c, T(n) = O(n^log_b(a))
。 (遞歸調用總數的增長速度快於每次調用的工作量減少的速度,因此總工作量由遞歸樹底層的調用次數決定。調用次數從 1 開始,乘以a
log_b(n) 次,因為 log_b(n) 是遞歸樹的深度。因此,總工作量 = a^log_b(n) = n^log_b(a))
a = b^c, T(n) = O(f(n)*log(n))
。 (調用次數的增長與每次調用工作量的減少相平衡。因此,遞歸樹每一層的工作量是恆定的,所以總工作量就是 f(n)*(depth of tree) = f(n) *log_b(n) = O(f(n)*log(n))
a < b^c, T(n) = O(f(n))
。 (每次調用的工作量減少的速度快於調用次數的增加量。因此,總工作量由遞歸樹頂層的工作量支配,即 f(n))
對於 mergeSort 的情況,我們已經看到 a = 2、b = 2 和 c = 1。作為 a = b^c,我們應用第二種情況:
T(n) = O(f(n)*log(n)) = O(n*log(n))
你完成了。 這似乎需要做很多工作,但是為 T(n) 提出一個遞歸式會變得越容易,而且一旦你有一個遞歸式,它就可以很快地檢查它屬於哪種情況,這使得主定理相當解決更復雜的分/治重復的有用工具。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.