1.1 頻度統計法。
頻度統計法指以程序中語句執行次數的多少作為算法時間度量分析的一種方法。通常情況下,算法的時間效率主要取決於程序中包含的語句條數和采用的控制結構這兩者的綜合效果。因此,最原始且最牢靠的方法是求出所有主要語句的頻度f(n),然后求所有頻度之和。
例如:如下形式的語句段:
for (i=1;i<n;i++)
{ y++;
for (j=0;j<(2*n);j++)
x++;
}
這個由兩個for語句構成的程序段,外循環的重復執行次數是n-1次,內循環的單趟重復執行次數是2×n次。因此,語句y++的頻度為n-1,語句x++的頻度為2×n×(n-1)。
所以, T(n)=O(∑f(n))=O(n-1+2×n2-2×n)
取增長最快的一項作為數量級,則 T(n)=O(n2)
該方法簡單,結果絕對精確,適用於大多數程序。但分析算法時間效率時往往只需計算出其大致數量級,此時,采用頻度估算法即可。
1.2 頻度估算法。
先找出對於所求解的問題來說是共同的原操作,並求出原操作的語句頻度f(n),然后直接以f(n)衡量T(n)。在使用頻度估算法時應注意到一個顯著的標志,就是原操作往往是最內層循環的循環體,並且,完成該操作所需的時間與操作數的具體取值無關。這種方法比較適合於帶有多重循環的程序。
例如:數學中求兩個矩陣乘法的常規方法是用了一個三重循環,如下:
for (i=1;i<=n;i++)
{ for (j=1;j<=n;j++)
{ c[i][j]=0;
for (k=1;k<=n;k++)
c[i][j]=c[i][j]+a[i][k]*b[k][j];
}
}
這個程序段的原操作是處於最內層循環的語句c[i][j]=c[i][j]+a[i][k]*b[k][j],該語句的重復執行次數即原操作的頻度是n3,可以直接得出:該算法的時間復雜度T(n)=O(n3)。
對於一些復雜的算法,可以將算法分解成容易估算的幾個部分,利用頻度估算法分別求出這幾部分的時間復雜度,然后利用求和的原則得到整個算法的時間復雜度。
頻度估算法的優點是結果較精確,方法簡單且易掌握,但對於原操作的頻度不易直接確定的程序,卻無能為力。
1.3 頻度未知數法。
當不能直接求出原操作的重復執行次數f(n),但通過對程序主要語句的分析,確信可以通過間接的方式計算得出。其基本規律是:首先將原操作的頻度設為一個未知數,然后根據原操作執行結束的條件及特征列方程求出它,如果結果是不等式,取其極大值。
比如程序段: i=s=0;
while (s<n)
{ i++; s+=i; }
分析:該程序段的原操作是語句s+=i,無法一眼看出原操作的頻度f(n)。為求得f(n),設循環體共執行了k次后結束,即令f(n)=k,則根據語義,可以得出
1+2+3+……+k>=n 且 1+2+3+……+(k-1)<n
由以上兩式,可得方程組
取其極值,並忽略常數對數量級的影響,可得 T(n)=O( )=O( )
1.4 列舉頻度歸納法。
程序中經常會出現帶有倍增型循環的情況。倍增循環指內循環的執行次數隨外循環控制變量而變化的多重循環結構。
例如: m=1;
for (i=1;i<=n;i++)
{ m=m*2;
for (j=1;j<=m;j++)
x++;
}
分析:當外循環變量i分別從1,2,……,一直取到n時,內循環的執行次數依次是2,4,8,……,2n,這是一個幾何級數序列,其中每一項的值是前一項的常數倍。
因此,
這種算法的特征是,原操作的頻度和一個變化的量有關,比如內循環的執行次數依賴於外循環的循環控制變量,或每執行一次循環,循環控制變量將被乘以(除以)一個常數。
再如: for (i=0;i<n;i++)
for (j=i;j<n;j++)
for (k=j;k<n;k++)
x++;
分析:由於最內層for循環控制語句執行1次,其循環體即原操作x++恰好執行了n-j次,因此,整個算法完成時共執行了 次x++語句,而
結論:當分析帶有倍增循環結構程序的運行時間開銷時,需要把每次執行循環的時間累加起來,其結果表達式往往就被轉化為一個級數求和的問題。
1.5 頻度期望值法。
當原操作的執行次數不僅依賴於問題的規模,而且隨原始數據集狀態的不同而不同時,往往需要根據原始數據的分布特點,考慮數據在某種概率分布下頻度的一個平均值。此時,即使問題規模相同,對於不同的特定輸入,其時間開銷也不同。
在這種情形下,考慮求符合某種概率分布情況下的原操作的平均頻度,然后以平均頻度的一個數量級作為算法的時間度量。
例如:順序查找算法
int Search_Seq(Table S,KeyType key)
// Table是查找表類型,KeyType表示關鍵字類型
{ S.elem[0].key=key;
for (i=S.length;S.elem[i]!=key;i--);
return i;
}
分析:該算法中的原操作是“將記錄的關鍵字和給定值進行比較”,但根據 for循環的判斷條件,比較的次數取決於待查記錄在查找表中的位置i。根據所查找數據在查找表中位置的不同,其時間開銷可能在一個很大的范圍內浮動。為求出時間復雜度,通常轉而求“其關鍵字和給定值進行過比較的記錄的個數的平均值”,即“比較次數”的平均頻度。一般來說,在進行研究時,為方便討論,對於經典的查找和排序算法,總是考慮“等概率”條件。
所以,各記錄的查找概率依次為:
第i個記錄的比較次數 是: =n-i+1
故,平均頻度
所以,T(n)= =O(n)
根據原始數據集的分布特點,有些查找問題的檢索概率不一定相等,但求解的方法相同,都是求某種概率分布下的一個期望值。對於原操作的執行次數依賴於原始數據排列情況的問題來說,內循環體的執行次數取決於外循環控制變量的情形非常常見,其求解方法也完全適用。例如:大多數靜態的排序算法。
需要說明的一點是:數據分布的特點對於很多查找算法效率都會有很大的影響,而平均情況分析並不總是可行。因為,首先要求了解清楚數據是如何分布的。對於這一類問題,有時候要根據各種可能出現的最壞情況來估算算法的時間復雜度。
1.6 遞歸算法時間復雜度的計算技巧。
遞歸過程的運行時間一般都能通過一個遞歸關系式得到很好的體現。根據對遞歸關系式的不同計算方法,將遞歸算法的求解方法提煉為如下兩種。
1.6.1擴展遞歸迭代法。
當需要找到一個遞歸問題的精確答案時,可采用一種遞歸擴展技術。其基本方法是:方程右邊較小的項根據定義被依次替代,如此反復擴展,直到得到一個沒有遞歸式的完整數列,從而將復雜的遞歸問題轉化為了新的求和問題。
例如: float fact(long int n)
{ if (n<0) return(-1);
else if (n==0 || n==1) return(1);
else return(n*fact(n-1));
}
分析:遞歸函數fact每遞歸調用自身一次,問題規模就減少1。該函數中出口語句的運行時間為O(1);調用返回的結果與輸入參數相乘,這個操作的運行時間是一個常量可以記為O(1)。因此,函數fact的時間代價就等於該常數加上執行遞歸調用的時間,可以表示成
C n<=1
T(n)= C為遞歸調用語句的運行時間,這里是常數O(1)
則,
=……
所以,函數fact的時間復雜度是O(n)。
利用遞歸本身的特點采用這種擴展技術求解遞歸程序的時間效率,是一種保守且可靠的方法。但有些問題,其求和序列的推導可能會是一項比較繁瑣而枯燥的純數學工作。遇到這種情況時,完全可以讓個人的經驗充分發揮作用,利用經驗去猜測答案。
1.6.2上下限猜測法。
先試着猜測答案,找出一個認為是正確的上下限估計,然后再去證明它。如果歸納證明成功,那么再試着收縮上下限;如果證明失敗,那么就放松限制重試;一旦上下限符合要求,就得到了所求的答案。
例如:以下方程描述歸並排序的運行時間,其數學的推導方法非常繁瑣,現用上下限猜測技術來估算其漸進時間復雜度。
1 n=2
T(n)=
不失一般性,先猜測這個遞歸有一個上限 O(n2),更准確地說,假定T(n)≤n2,通過歸納來證明這個假定是否正確。如果正確,繼續收縮上限,猜測一個更小的估計。
為了使計算簡便,假定n是2的乘方。初始情況:T(2)=1≤22,顯然成立。
假設當i≤n時 T(i)≤i2 成立,要證明對於所有的n=2N,N≥1,T(n) ≤n2能夠得到T(2n) ≤(2n)2。
而 T(2n)=2T(n)+2n≤2n2+2n≤4n2≤(2n)2 至此命題得證。
所以,猜測T(n)≤O(n2)是正確的。
但是O(n2)可能是一個很高的估計。如果猜測更小一些,例如T(n)≤cn(c為某個常數),很明顯,因為c2n=2cn,沒有為額外的代價n留下余地,使待排序的兩塊歸並在一起,因此T(n)≤cn不可能成立。這樣就可以初步得知,真正的代價一定在cn與n2之間。
繼續嘗試T(n)≤nlog2n。初始情況: T(2)=1≤(2•log22)=2。歸納假設T(n)≤nlog2n,那么:T(2n)=2T(n)+2n≤2nlog2n+2n≤2n(log2n+1) ≤2nlog22n
類似地,還可以證明T(n)≥(nlog2n)。所以,T(n)就是O(nlog2n)。
在求解漸近時間復雜度時,這種猜測技術是一種很有用的技術,當尋找精確解時,就不適用了。
2. 結束語
提倡在解決問題時,首先要詳細分析算法的特性及特征,然后再按照不同的特性采用合適的方法區別對待,具體問題具體分析,從而避免不必要的代價和周折,達到快速而准確求解的目的。