演算法複雜度分析
一 、時間複雜度
演算法複雜度分為時間複雜度和空間複雜度。其作用: 時間複雜度是度量演算法執行的時間長短;而空間複雜度是度量演算法所需儲存空間的大小。任何演算法執行所需要的時間幾乎總是取決於他所處理的資料量,在這裡我們主要說時間複雜度。對於一個給定計算機的演算法程式,我們能畫出執行時間的函式圖。一個演算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
1. 一般情況下,演算法的基本操作重複執行的次數是模組n的某一個函式f(n),因此,演算法的時間複雜度記做:T(n)=O(f(n))
分析:隨著模組n的增大,演算法執行的時間的增長率和f(n)的增長率成正比,所以f(n)越小,演算法的時間複雜度越低,演算法的效率越高。- for(i=1;i<=n;++i)
- {
- for(j=1;j<=n;++j)
- {
- c[ i ][ j ]=0; //該步驟屬於基本操作 執行次數:n的平方 次
-
for(k=1;k<=n;++k)
- c[ i ][ j ]+=a[ i ][ k ]*b[ k ][ j ]; //該步驟屬於基本操作 執行次數:n的三次方 次
- }
- }
按數量級遞增排列,常見的時間複雜度有: 常數階O(1),對數階O(log2n),線性階O(n), 線性對數階O(nlog2n),平方階O(n^2),立方階O(n^3),..., k次方階O(n^k), 指數階O(2^n) 。隨著問題規模n的不斷增大,上述時間複雜度不斷增大,演算法的執行效率越低。
根據定義,可以歸納出基本的計算步驟
1. 計算出基本操作的執行次數T(n)
基本操作即演算法中的每條語句(以;號作為分割),語句的執行次數也叫做語句的頻度。在做演算法分析時,一般預設為考慮最壞的情況。
2. 計算出T(n)的數量級
求T(n)的數量級,只要將T(n)進行如下一些操作:
忽略常量、低次冪和最高次冪的係數,令f(n)=T(n)的數量級。
3. 用大O來表示時間複雜度
當n趨近於無窮大時,如果lim(T(n)/f(n))的值為不等於0的常數,則稱f(n)是T(n)的同數量級函式。記作T(n)=O(f(n))。
一個示例:
- int num1, num2;
- for(int i=0; i<n; i++){
- num1 += 1;
- for(int j=1; j<=n; j*=2){
- num2 += num1;
- }
- }
1.
語句int num1, num2;的頻度為1;
語句i=0;的頻度為1;
語句i<n; i++; num1+=1; j=1; 的頻度為n;
語句j<=n; j*=2; num2+=num1;的頻度為n*log2n;
T(n) = 2 + 4n + 3n*log2n
2.
忽略掉T(n)中的常量、低次冪和最高次冪的係數,f(n) = n*log2n
3.
lim(T(n)/f(n)) = (2+4n+3n*log2n) / (n*log2n)
= 2*(1/n)*(1/log2n) + 4*(1/log2n) + 3
當n趨向於無窮大,1/n趨向於0,1/log2n趨向於0
所以極限等於3。T(n) = O(n*log2n)簡化的計算步驟
再來分析一下,可以看出,決定演算法複雜度的是執行次數最多的語句,這裡是num2 += num1,一般也是最內迴圈的語句。並且,通常將求解極限是否為常量也省略掉?
於是,以上步驟可以簡化為:
1. 找到執行次數最多的語句
2. 計算語句執行次數的數量級
3. 用大O來表示結果
繼續以上述演算法為例,進行分析:
1.
執行次數最多的語句為num2 += num1
2.
T(n) = n*log2n
f(n) = n*log2n
3.
// lim(T(n)/f(n)) = 1
T(n) = O(n*log2n)
二、插入排序演算法的時間複雜度
現在研究一下插入排序演算法的執行時間,按照習慣,輸入長度LEN
以下用n表示。設迴圈中各條語句的執行時間分別是c1、c2、c3、c4、c5這樣五個常數:
- void insertion_sort(void) 執行時間
- {
- int i, j, key;
- for (j = 1; j < LEN; j++) {
- key = a[j]; c1
- i = j - 1; c2
- while (i >= 0 && a[i] > key) {
- a[i+1] = a[i]; c3
- i--; c4
- }
- a[i+1] = key; c5
- }
- }
顯然外層for
迴圈的執行次數是n-1次,假設內層的while
迴圈執行m次,則總的執行時間粗略估計是(n-1)*(c1+c2+c5+m*(c3+c4))。當然,for
和while
後面()括號中的賦值和條件判斷的執行也需要時間,而我沒有設一個常數來表示,這不影響我們的粗略估計。
這裡有一個問題,m不是個常數,也不取決於輸入長度n,而是取決於具體的輸入資料。在最好情況下,陣列a
的原始資料已經排好序了,while
迴圈一次也不執行,總的執行時間是(c1+c2+c5)*n-(c1+c2+c5),可以表示成an+b的形式,是n的線性函式(Linear Function)。那麼在最壞情況(Worst
Case)下又如何呢?所謂最壞情況是指陣列a
的原始資料正好是從大到小排好序的,請讀者想一想為什麼這是最壞情況,然後把上式中的m替換掉算一下執行時間是多少。
陣列a
的原始資料屬於最好和最壞情況的都比較少見,如果原始資料是隨機的,可稱為平均情況(Average Case)。如果原始資料是隨機的,那麼每次迴圈將已排序的子序列a[1..j-1]與新插入的元素key
相比較,子序列中平均都有一半的元素比key
大而另一半比key
小,請讀者把上式中的m替換掉算一下執行時間是多少。最後的結論應該是:在最壞情況和平均情況下,總的執行時間都可以表示成an2+bn+c的形式,是n的二次函式(Quadratic
Function)。
在分析演算法的時間複雜度時,我們更關心最壞情況而不是最好情況,理由如下:
-
最壞情況給出了演算法執行時間的上界,我們可以確信,無論給什麼輸入,演算法的執行時間都不會超過這個上界,這樣為比較和分析提供了便利。
-
對於某些演算法,最壞情況是最常發生的情況,例如在資料庫中查詢某個資訊的演算法,最壞情況就是資料庫中根本不存在該資訊,都找遍了也沒有,而某些應用場合經常要查詢一個資訊在資料庫中存在不存在。
-
雖然最壞情況是一種悲觀估計,但是對於很多問題,平均情況和最壞情況的時間複雜度差不多,比如插入排序這個例子,平均情況和最壞情況的時間複雜度都是輸入長度n的二次函式。
比較兩個多項式a1n+b1和a2n2+b2n+c2的值(n取正整數)可以得出結論:n的最高次指數是最主要的決定因素,常數項、低次冪項和係數都是次要的。比如100n+1和n2+1,雖然後者的係數小,當n較小時前者的值較大,但是當n>100時,後者的值就遠遠大於前者了。如果同一個問題可以用兩種演算法解決,其中一種演算法的時間複雜度為線性函式,另一種演算法的時間複雜度為二次函式,當問題的輸入長度n足夠大時,前者明顯優於後者。因此我們可以用一種更粗略的方式表示演算法的時間複雜度,把係數和低次冪項都省去,線性函式記作Θ(n),二次函式記作Θ(n2)。
Θ(g(n))表示和g(n)同一量級的一類函式,例如所有的二次函式f(n)都和g(n)=n2屬於同一量級,都可以用Θ(n2)來表示,甚至有些不是二次函式的也和n2屬於同一量級,例如2n2+3lgn。“同一量級”這個概念可以用下圖來說明(該圖出自[演算法導論]):
圖 11.2. Θ-notation
如果可以找到兩個正的常數c1和c2,使得n足夠大的時候(也就是n≥n0的時候)f(n)總是夾在c1g(n)和c2g(n)之間,就說f(n)和g(n)是同一量級的,f(n)就可以用Θ(g(n))來表示。
以二次函式為例,比如1/2n2-3n,要證明它是屬於Θ(n2)這個集合的,我們必須確定c1、c2和n0,這些常數不隨n改變,並且當n≥n0以後,c1n2≤1/2n2-3n≤c2n2總是成立的。為此我們從不等式的每一邊都除以n2,得到c1≤1/2-3/n≤c2。見下圖:
圖 11.3. 1/2-3/n
這樣就很容易看出來,無論n取多少,該函式一定小於1/2,因此c2=1/2,當n=6時函式值為0,n>6時該函式都大於0,可以取n0=7,c1=1/14,這樣當n≥n0時都有1/2-3/n≥c1。通過這個證明過程可以得出結論,當n足夠大時任何an2+bn+c都夾在c1n2和c2n2之間,相對於n2項來說bn+c的影響可以忽略,a可以通過選取合適的c1、c2來補償。
幾種常見的時間複雜度函式按數量級從小到大的順序依次是:Θ(lgn),Θ(sqrt(n)),Θ(n),Θ(nlgn),Θ(n2),Θ(n3),Θ(2n),Θ(n!)。其中,lgn通常表示以10為底n的對數,但是對於Θ-notation來說,Θ(lgn)和Θ(log2n)並無區別(想一想這是為什麼),在演算法分析中lgn通常表示以2為底n的對數。可是什麼演算法的時間複雜度裡會出現lgn呢?回顧插入排序的時間複雜度分析,無非是迴圈體的執行時間乘以迴圈次數,只有加和乘運算,怎麼會出來lg呢?下一節歸併排序的時間複雜度裡面就有lg,請讀者留心lg運算是從哪出來的。
除了Θ-notation之外,表示演算法的時間複雜度常用的還有一種Big-O notation。我們知道插入排序在最壞情況和平均情況下時間複雜度是Θ(n2),在最好情況下是Θ(n),數量級比Θ(n2)要小,那麼總結起來在各種情況下插入排序的時間複雜度是O(n2)。Θ的含義和“等於”類似,而大O的含義和“小於等於”類似。受記憶體管理機影響,指令的執行時間不一定是常數,但執行時間的上界(Upper Bound)肯定是常數,我們這裡假設語句的執行時間是常數只是一個粗略估計。
三、常用的演算法的時間複雜度和空間複雜度
排序法 |
最差時間分析 | 平均時間複雜度 | 穩定度 | 空間複雜度 |
氣泡排序 | O(n2) | O(n2) | 穩定 | O(1) |
快速排序 | O(n2) | O(n*log2n) | 不穩定 | O(log2n)~O(n) |
選擇排序 | O(n2) | O(n2) | 穩定 | O(1) |
二叉樹排序 | O(n2) | O(n*log2n) | 不一頂 | O(n) |
插入排序 |
O(n2) | O(n2) | 穩定 | O(1) |
堆排序 | O(n*log2n) | O(n*log2n) | 不穩定 | O(1) |
希爾排序 | O | O | 不穩定 | O(1) |