1. 程式人生 > >13、【演算法】演算法複雜度分析

13、【演算法】演算法複雜度分析

一、演算法的時間複雜度分析

1、時間複雜度的定義

    在進行演算法分析時,演算法中基本操作語句重複執行的次數是問題規模n的某個函式,用T(n)表示,若有某個輔助函式f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函式,記作T(n)=O(f(n)),它稱為演算法的漸進時間複雜度,簡稱為時間複雜度

    T (n) = Ο(f (n)) 表示存在一個常數C,使得在當n趨於正無窮時總有 T (n) ≤ C * f(n)。簡單來說,就是T(n)在n趨於正無窮時最大也就跟f(n)差不多大。也就是說當n趨於正無窮時T (n)的上界是C * f(n)。其雖然對f(n)沒有規定,但是一般都是取儘可能簡單的函式。例如,O(2n2

+n +1) = O (3n2+n+3) = O (7n2 + n) = O ( n2 ) ,一般都只用O(n2)表示就可以了。注意到大O符號裡隱藏著一個常數C,所以f(n)裡一般不加係數。如果把T(n)當做一棵樹,那麼O(f(n))所表達的就是樹幹,只關心其中的主幹,其他的細枝末節全都拋棄不管。

    這樣用大寫O()來體現演算法時間複雜度的記法,我們稱之為大0表示法。大O表示法O(f(n)中的f(n)的值可以為1、n、logn、n²等,因此我們可以將O(1)、O(n)、O(logn)、O(n²)分別可以稱為常數階、線性階、對數階和平方階。

    演算法複雜度可以從最理想情況、平均情況和最壞情況

三個角度來評估,由於平均情況大多和最壞情況持平,而且評估最壞情況也可以避免後顧之憂,因此一般情況下,我們設計演算法時都要直接估算最壞情況的複雜度。

2、求解演算法時間複雜度的具體步驟(大O階的推導)

(1)找出演算法中的基本語句
  演算法中執行次數最多的那條語句就是基本語句,通常是最內層迴圈的迴圈體。
  
(2)計算基本語句的執行次數的數量級
  只需計算基本語句執行次數的數量級,這就意味著只要保證基本語句執行次數的函式中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的係數。這樣能夠簡化演算法分析,並且使注意力集中在最重要的一點上:增長率。
  
(3)用大Ο記號表示演算法的時間效能
  將基本語句執行次數的數量級放入大Ο記號中。

    如果演算法中包含巢狀的迴圈,則基本語句通常是最內層的迴圈體,如果演算法中包含並列的迴圈,則將並列迴圈的時間複雜度相加。例如:

for (i=1; i<=n; i++)
	x++;
for (i=1; i<=n; i++)
	for (j=1; j<=n; j++)
		x++;

    第一個for迴圈的時間複雜度為Ο(n),第二個for迴圈的時間複雜度為Ο(n2),則整個演算法的時間複雜度為Ο(n+n2)=Ο(n2)。

    Ο(1)表示基本語句的執行次數是一個常數,一般來說,只要演算法中不存在迴圈語句,其時間複雜度就是Ο(1)。其中Ο(log2n)、Ο(n)、 Ο(nlog2n)、Ο(n2)和Ο(n3)稱為多項式時間,而Ο(2n)和Ο(n!)稱為指數時間。電腦科學家普遍認為前者(即多項式時間複雜度的演算法)是有效演算法,把這類問題稱為P(Polynomial,多項式)類問題,而把後者(即指數時間複雜度的演算法)稱為NP(Non-Deterministic Polynomial, 非確定多項式)問題。

3、簡單的程式分析法則

    (1)對於一些簡單的輸入輸出語句或賦值語句,近似認為需要O(1)時間

    (2)對於順序結構,需要依次執行一系列語句所用的時間可採用大O下"求和法則"

    求和法則:是指若演算法的2個部分時間複雜度分別為 T1(n)=O(f(n))和 T2(n)=O(g(n)),則 T1(n)+T2(n)=O(max(f(n), g(n)))

特別地,若T1(m)=O(f(m)), T2(n)=O(g(n)),則 T1(m)+T2(n)=O(f(m) + g(n))

    (3)對於選擇結構,如if語句,它的主要時間耗費是在執行then字句或else字句所用的時間,需注意的是檢驗條件也需要O(1)時間

    (4)對於迴圈結構,迴圈語句的執行時間主要體現在多次迭代中執行迴圈體以及檢驗迴圈條件的時間耗費,一般可用大O下"乘法法則"

    乘法法則: 是指若演算法的2個部分時間複雜度分別為 T1(n)=O(f(n))和 T2(n)=O(g(n)),則 T1*T2=O(f(n)*g(n))

    (5)對於複雜的演算法,可以將它分成幾個容易估算的部分,然後利用求和法則和乘法法則技術整個演算法的時間複雜度

    另外還有以下2個運演算法則:(1) 若g(n)=O(f(n)),則O(f(n))+ O(g(n))= O(f(n));(2) O(Cf(n)) = O(f(n)),其中C是一個正常數

4、複雜度分析示例

常數階

  int sum = 0, n = 100;       /*執行一次*/
  sum = (1 + n) * n / 2;      /*執行一次*/
  printf("%d",sum);           /*執行一次*/

    上面演算法的執行的次數的函式為f(n)=3,根據推導大O階的規則1,我們需要將常數3改為1,則這個演算法的時間複雜度為O(1)
    這種與問題的大小無關(n的多少),執行時間恆定的演算法,我們稱之為具有O(1)的時間複雜度,又叫常數階。對於分支結構而言,無論是真,還是假,執行的次數都是恆定的,不會隨著n 的變大而發生變化,所以單純的分支結構(不包含在迴圈結構中),其時間複雜度也是0(1)。

線性階
    線性階的迴圈結構會複雜很多。要確定某個演算法的階次,我們常常需要確定某個特定語句或某個語句集執行的次數。因此,我們要分析演算法的複雜度,關鍵就是要分析迴圈結構的執行情況。

int i;      
for(i = 0; i < n; i++){
    /*時間複雜度為O(1)的程式步驟序列*/
}

上面這段程式碼,它的迴圈的時間複雜度為O(n), 因為迴圈體中的程式碼須要執行n次。

對數階

int count = 1;      
while (count < n){
   count = count * 2;
  /*時間複雜度為O(1)的程式步驟序列*/
}

    由於每次count乘以2之後,就距離n更近了一分。 也就是說,有多少個2相乘後大於n,則會退出迴圈。 由2x=n 得到x=log2n。 所以這個迴圈的時間複雜度為O(log2n)。

nlong2n階

int binarySearch(int *arr, int value, int low, int high)
{
    int mid = low + (high - low)/2;
    if(arr[mid] == value)
        return mid;
    if(arr[mid] > value)
        return binarySearch(arr, value, low, mid-1);
    if(arr[mid] < value)
        return binarySearch(arr, value, mid+1, high);
}

    我們可以把整個有序陣列比作一個二叉樹,根節點的左子樹都小於根,右子樹都大於根,二叉樹有N個結點,則二叉樹的高度就是:h≈log2N

    顯然有N個結點的M叉樹的高度就是logMN。

    此時最壞的情況就是把所有的結點都查找了一遍,即二分查詢的時間複雜度就是:O(log2N);

平方階

  for(int i=0;i<n;i++){   
      for(int j=0;j<n;i++){
         //複雜度為O(1)的演算法
         ... 
      }
  }

    內層迴圈的時間複雜度在講到線性階時就已經得知是O(n),現在經過外層迴圈n次,那麼這段演算法的時間複雜度則為O(n²)。 如果外迴圈的迴圈次數改為了m,時間複雜度就變為O(m*n)。所以我們可以總結得出,迴圈的時間複雜度等於迴圈體的複雜度乘以該迴圈執行的次數。
    接下來我們看看下面演算法的時間複雜度:

  for(int i=0;i<n;i++){   
      for(int j=i;j<n;i++){
         //複雜度為O(1)的演算法
         ... 
      }
  }

    需要注意的是內迴圈中int j=i,而不是int j=0。當i=0時,內迴圈執行了n次;i=1時內迴圈執行了n-1次,當i=n-1時執行了1次,我們可以推算出總的執行次數為:

	n+(n-1)+(n-2)+(n-3)+……+1
	=(n+1)+[(n-1)+2]+[(n-2)+3]+[(n-3)+4]+……
	=(n+1)+(n+1)+(n+1)+(n+1)+……
	=(n+1)n/2
	=n(n+1)/2
	=n²/2+n/2

    根據此前講過的推導大O階的規則的第二條:只保留最高階,因此保留n²/2。根據第三條去掉和這個項的常數,則去掉1/2,最終這段程式碼的時間複雜度為O(n²)。

立方階

int i, j; 
for(i = 1; i < n; i++) 
	for(j = 1; j < n; j++) 
		for(j = 1; j < n; j++){ 
			/*時間複雜度為O(1)的程式步驟序列*/ 
		}

    這裡迴圈了(12+22+32+……+n2) = n(n+1)(2n+1)/6次,按照上述大O階推導方法,時間複雜度為O(n^3)。

5、常見時間複雜度

常見的時問複雜度如表所示。

常用的時間複雜度所耗費的時間從小到大依次是:

O(1)<O(logn)<O(n)<O(nlogn)<O()<O()<O(2)<O(n!)
6、常見演算法的時間複雜度

二、演算法的空間複雜度分析

    我們在寫程式碼時,完全可以用空間來換取時間,比如說,要判斷某某年是不是閏年,你可能會花一點心思寫了一個演算法,而且由於是一個演算法,也就意味著,每次給一個年份,都是要通過計算得到是否是閏年的結果。 還有另一個辦法就是,事先建立一個有2050個元素的陣列(年數略比現實多一點),然後把所有的年份按下標的數字對應,如果是閏年,此陣列項的值就是1,如果不是值為0。這樣,所謂的判斷某一年是否是閏年,就變成了查詢這個陣列的某一項的值是多少的問題。此時,我們的運算是最小化了,但是硬碟上或者記憶體中需要儲存這2050個0和1。這是通過一筆空間上的開銷來換取計算時間的小技巧。到底哪一個好,其實要看你用在什麼地方。
    演算法的空間複雜度通過計算演算法所需的儲存空間實現,演算法空間複雜度的計算公式記作:S(n)= O(f(n)),其中,n為問題的規模,f(n)為語句關於n所佔儲存空間的函式。
    一般情況下,一個程式在機器上執行時,除了需要儲存程式本身的指令、常數、變數和輸入資料外,還需要儲存對資料操作的儲存單元,若輸入資料所佔空間只取決於問題本身,和演算法無關,這樣只需要分析該演算法在實現時所需的輔助單元即可。若演算法執行時所需的輔助空間相對於輸入資料量而言是個常數,則稱此演算法為原地工作,空間複雜度為0(1)。
    通常, 我們都使用"時間複雜度"來指執行時間的需求,使用"空間複雜度"指空間需求。當不用限定詞地使用"複雜度’時,通常都是指時間複雜度。