演算法複雜性漸近階的分析
前兩段講的是演算法複雜性漸近階的概念和對它進行分析的重要性。本段要講如何具體地分析一個演算法的複雜性的漸近階,給出一套可操作的規則。演算法最終要落實到用某種程式設計語言(如Pascal)編寫成的程式。因此演算法複雜性漸近階的分析可代之以對錶達該演算法的程式的複雜性漸近階的分析。
如前所提出,對於演算法的複雜性,我們只考慮最壞、最好和平均三種情況,而通常又著重於最壞情況。為了明確起見,本段限於針對最壞情況。
仍然以時間複雜性為例。這裡給出分析時間複雜性漸近階的八條規則。這八條規則已覆蓋了用Pascal語言程式所能表達的各種演算法在最壞情況下的時間複雜性漸近階的分析。
在逐條地列出並解釋這入條規則之前,應該指出,當我們剖析器的某一局部(如一個語句,一個分程式,一個程式段,一個過程或函數)時,可以用具體程式的輸入的規模N作為複雜性函數的自變數,也可以用局部的規模參數作為自變數。但是,作為最終結果的整體程式的複雜性函數只能以整體程式的輸入規模為自變數。
對於串列的演算法,相應的Pascal程式是一個串列的Pascal語句序列,因此,很明顯,該演算法的時間複雜性(即所需要的時間)等於相應的Pascal程式的每一個語句的時間複雜性(即所需要的時間)之和。所以,如果執行Pascal語句中的每一種語句所需要的時間都有計量的規則,那麼,執行一個程式,即執行一個演算法所需要的時間的計量便只是一個代數問題。接著,應用本節第三段所提供的Ο、Ω和θ等運算規則就可以分析出演算法時間複雜性的漸近階。
因此,我們的時間計量規則只需要針對Pascal有限的幾種基本運算和幾種基本語句。下面是這些規則的羅列和必要的說明。
-
規則(1)
賦值、比較、算術運算、邏輯運算、讀寫單個常量或單個變數等,只需要1個單位時間。
-
規則(2)
條件陳述式"if C then S1 else S2"只需要Tc+max(Ts1,Ts2)的時間,其中Tc是計算條件運算式C需要的時間,而Ts1和Ts2分別是執行語句S1和S2需要的時間。
-
規則(3)
選擇語句"Case A of a1:S1; a2:S2; … ;am:Sm; end",需要max(Ts1, Ts2,…,Tsm)的時間,其中Tsii是執行語句Si所需要的時間,i=l,2,…,m。
-
規則(4)
訪問一個數組的單個分量或一個記錄的單個域,只需要1個單位時間。
-
規則(5)
執行一個for迴圈語句需要的時間等於執行該迴圈體所需要的時間乘上迴圈的次數。
-
規則(6)
執行一個while迴圈語句"while C do S"或一個repeat迴圈語句" repeat S until C",需要的時間等於計算條件運算式C需要的時間與執行迴圈S體需要的時間之和乘以迴圈的次數。與規則5不同,這裡的迴圈次數是隱含的。
例如,b_search函數中的while迴圈語句。按規則(1)-(4),計算條件運算式" (not found)and(U≥=L)"與執行迴圈體
I:=(U+L)div 2;if c=A[I] then found:=trueelse if c>A[I] then L:=I+1else U:=I-1;
只需要θ(1)時間,而迴圈次數為logm,所以,執行此while語句只需要θ(logm)時間。
在許多情況下,運用規則(5)和(6)常常須要藉助具體演算法的內涵來確定迴圈的次數,才不致使時間的估計過於保守。這裡舉一個例子。
考察程式段:
| |
|
Size:=m; |
1 |
i:=1; |
1 |
while i<n do |
|
begin |
|
i:=i+1; |
|
S1; |
θ(n) |
if Size>0 then |
1 |
begin |
|
在1到Size的範圍內任選一個數賦值給t; |
θ(1) |
Size:=Size-t; |
2 |
for j:=l to t do |
|
S2 |
θ(n) |
end; |
|
end; |
|
| |
|
程式在各行右端頂格處標註著執行相應各行所需要的時間。如果不對演算法的內涵作較深入的考察,只看到1≤t≤Size≤m,就草率地估計while的內迴圈for的迴圈次數為Ο(m),那麼,程式在最壞情況下的時間複雜性將被估計為Ο(n 2+m·n 2)。反之,如果對演算法的內涵認真地分析,結果將兩樣。事實上,在while的迴圈體內t是動態,size也是動態,它們都取決while的迴圈參數i,即t=t(i)記為ti;size=size(i)記為sizei ,i=l,2,…,n-1。對於各個i,1≤i≤n-1,ti與m的關係是隱含的,這給準確地計算for迴圈的迴圈體S2被執行的次數帶來困難。上面的估計比較保守的原因在於我們把S2的執行次數的統計過於局部化。如果不局限於for迴圈,而是在整個程式段上統計S2被執行的總次數,那麼,這個總次數等於,又根據演算法中ti的取法及sizei+1=sizei-ti,i=1,2,…,n-1 有sizen=size1-。最後利用size1=m和sizen=0得到=m 。於是在整個程式段上,S2被執行的總次數為m,所需要的時間為θ(mn)。執行其他語句所需要的時間直接運用規則(l)-(6)容易計算。累加起來,整個程式段在最壞情況下時間複雜性漸近階為θ(n 2+mn)。這個結果顯然比前面粗糙的估計準確。
-
規則(7)
對於goto語句。在Pascal中為了便於表達從迴圈體的中途跳轉到迴圈體的結束或跳轉到迴圈語句的後面語句,引入goto語句。如果我們的程式按照這一初衷使用goto語句,那麼,在時間複雜性分析時可以假設它不需要任何額外的時間。因為這樣做既不會低估也不會高估程式在最壞情況下的已耗用時間的階。如果有的程式濫用了goto語句,即控制轉移到前面的語句,那麼情況將變得複雜起來。當這種轉移造成某種迴圈時,只要與別的迴圈不交叉,保持迴圈的內外嵌套,則可以比照規則(1)-(6)進行分析。當由於使用goto語句而使程式結構混亂時,建議改寫程式然後再做分析。
-
規則(8)
對於程序呼叫和函數調用語句,它們需要的時間包括兩部分,一部分用於實現控制轉移,另一部分用於執行過程(或函數)本身,這時可以根據過程(或函數)調用的層次,由裡向外運用規則(l)-(7)進行分析,一層一層地剝,直到計算出最外層的已耗用時間便是所求。如果過程(或函數)出現直接或間接的遞迴調用,則上述由裡向外逐層剝的分析行不通。這時我們可以對其中的各個遞迴過程(或函數),所需要的時間假設為一個相應規模的待定函數。然後一一根據過程(或函數)的內涵建立起這些待定函數之間的遞迴關係得到遞迴方程。最後用求遞迴方程解的漸進階的方法確定最壞情況下的複雜性的漸進階。
遞迴方程的種類很多,求它們的解的漸近階的方法也很多,我們將在下一段比較系統地給予介紹。本段只舉一個簡單遞迴過程(或函數)的例子來說明如何建立相應的遞迴方程,同時不加推導地給出它們在最壞情況下的時間複雜性的漸近階。
例:再次考察函數b_search,這裡將它改寫成一個遞迴函式。為了簡明,我們已經運用前面的規則(l)-(6),統計出執行各行語句所需要的時間,並標註在相應行的右端:
|
|
Function b_search(C,L,U:integer):integer; |
單位時間數 |
var index,element:integer; |
|
begin |
|
if (U<L) then |
1 |
b_search:=0; |
1 |
else |
|
begin |
|
index:=(L+U) div 2; |
3 |
element:=A[index]; |
2 |
if element=C then |
1 |
b_search:=index |
1 |
else if element>C then |
|
b_search:=b_search(C,L,index-1) |
3+T(m/2) |
else |
|
b_search:=b_search(C,index+1,U); |
3+T(m/2) |
end; |
|
end; |
|
| |
|
其中T(m)是當問題的規模U-L+1=m時b_search在最壞情況下(這時,數組A[L..U]中沒有給定的C)的時間複雜性。根據規則(l)-(8),我們有:
或化簡為
這是一個關於T(m)的遞迴方程。用下一段將介紹的迭代法,容易解得:
T(m)=11logm +l3=θ(logm)
在結束這一段之前,我們要提一下關於演算法在最壞情況下的空間複雜性分析。我們照樣可以給出與分析時間複雜性類似的規則。這裡不贅述。然而應該指出,在出現過程(或函數)遞迴調用時要考慮到其中隱含的儲存空間的額外開銷。因為現有的實現過程(或函數)遞迴調用的編程技術需要一個隱含的、額外(即不出現在程式的說明中)的棧來支援。過程(或函數)的遞迴調用每深人一層就把本層的現場局部資訊及調用的返回地址存放在棧頂備用,直到調用的最裡層。因此遞迴調用一個過程(或函數)所需要的額外儲存空間的大小即棧的規模與遞迴調用的深度成正比,其比例因素等於每深入一層需要儲存的資料量。比如本段前面所舉的遞迴函式b_search,在最壞情況下,遞迴調用的深度為logm,因而在最壞情況下調用它所需要的額外儲存空間為θ(logm)。