1. 程式人生 > >演算法複雜度分析

演算法複雜度分析

一 、時間複雜度

   演算法複雜度分為時間複雜度空間複雜度。其作用: 時間複雜度是度量演算法執行的時間長短;而空間複雜度是度量演算法所需儲存空間的大小任何演算法執行所需要的時間幾乎總是取決於他所處理的資料量,在這裡我們主要說時間複雜度。對於一個給定計算機的演算法程式,我們能畫出執行時間的函式圖。一個演算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)

1. 一般情況下,演算法的基本操作重複執行的次數是模組n的某一個函式f(n),因此,演算法的時間複雜度記做:T(n)=O(f(n))

  分析:隨著模組n的增大,演算法執行的時間的增長率和f(n)的增長率成正比,所以f(n)越小,演算法的時間複雜度越低,演算法的效率越高。
2. 在計算時間複雜度的時候,先找出演算法的基本操作,然後根據相應的各語句確定它的執行次數,再找出T(n)的同數量級(它的同數量級有以下:1<Log2n <n <nLog2n <n的平方<n的三次方<2的n次方<n!),找出後,f(n)=該數量級,若T(n)/f(n)求極限可得到一常數c,則時間複雜度T(n)=O(f(n)),例:
  1. for(i=1;i<=n;++i)  
  2. {  
  3. for(j=1;j<=n;++j)  
  4. {  
  5. c[ i ][ j ]=0//該步驟屬於基本操作 執行次數:n的平方 次
  6. for(k=1;k<=n;++k)  
  7. c[ i ][ j ]+=a[ i ][ k ]*b[ k ][ j ]; //該步驟屬於基本操作 執行次數:n的三次方 次
  8. }  
  9. }  
  則有 T(n)= n的平方+n的三次方,根據上面括號裡的同數量級,我們可以確定 n的三次方 為T(n)的同數量級,則有f(n)= n的三次方,然後根據T(n)/f(n)求極限可得到常數c。則該演算法的 時間複雜度:T(n)=O(n^3) 注:n^3即是n的3次方。 3.在pascal中比較容易理解,容易計算的方法是:看看有幾重for迴圈,只有一重則時間複雜度為O(n),二重則為O(n^2),依此類推,如果有二分則為O(logn),二分例如快速冪、二分查詢,如果一個for迴圈套一個二分,那麼時間複雜度則為O(nlogn)。

  按數量級遞增排列,常見的時間複雜度有:   常數階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))。

   一個示例:

  1. int num1, num2;  
  2.  for(int i=0; i<n; i++){   
  3.      num1 += 1;  
  4.     for(int j=1; j<=n; j*=2){   
  5.         num2 += num1;  
  6.     }  
  7. }   
分析:

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這樣五個常數:

  1. void insertion_sort(void)           執行時間  
  2. {  
  3.     int i, j, key;  
  4.     for (j = 1; j < LEN; j++) {  
  5.         key = a[j];         c1  
  6.         i = j - 1;          c2  
  7.         while (i >= 0 && a[i] > key) {  
  8.             a[i+1] = a[i];      c3  
  9.             i--;            c4  
  10.         }  
  11.         a[i+1] = key;           c5  
  12.     }  
  13. }  

         顯然外層for迴圈的執行次數是n-1次,假設內層的while迴圈執行m次,則總的執行時間粗略估計是(n-1)*(c1+c2+c5+m*(c3+c4))。當然,forwhile後面()括號中的賦值和條件判斷的執行也需要時間,而我沒有設一個常數來表示,這不影響我們的粗略估計。

這裡有一個問題,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)。

在分析演算法的時間複雜度時,我們更關心最壞情況而不是最好情況,理由如下:

  1. 最壞情況給出了演算法執行時間的上界,我們可以確信,無論給什麼輸入,演算法的執行時間都不會超過這個上界,這樣為比較和分析提供了便利。

  2. 對於某些演算法,最壞情況是最常發生的情況,例如在資料庫中查詢某個資訊的演算法,最壞情況就是資料庫中根本不存在該資訊,都找遍了也沒有,而某些應用場合經常要查詢一個資訊在資料庫中存在不存在。

  3. 雖然最壞情況是一種悲觀估計,但是對於很多問題,平均情況和最壞情況的時間複雜度差不多,比如插入排序這個例子,平均情況和最壞情況的時間複雜度都是輸入長度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

Θ-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

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)