1. 程式人生 > >資料結構-時間複雜度計算詳解--向李紅老師的資料結構低頭 :)

資料結構-時間複雜度計算詳解--向李紅老師的資料結構低頭 :)

今天早上突然想總結一下資料結構的時間複雜度的知識。
之前學了很多遍,但是一直沒有總結,所以之前參考了Algorithm還有清華大學出版的那個資料結構書,今天早上花了幾個小時好好的總結一下,也送給三班的同學們。
演算法的時間複雜度定義為:
在進行演算法分析時,語句總的執行次數T(n)是關於問題規模n的函式,進而分析T(n)隨n的變化情況並確定T(n)的數量級。演算法的時間複雜度,也就是演算法的時間量度,記作:T(n}=0(f(n))。它表示隨問題規模n的增大,演算法執行時間的埔長率和 f(n)的埔長率相同,稱作演算法的漸近時間複雜度,簡稱為時間複雜度。其中f( n)是問題規橫n的某個函式。

根據定義,求解演算法的時間複雜度的具體步驟是:
  ⑴ 找出演算法中的基本語句;

  演算法中執行次數最多的那條語句就是基本語句,通常是最內層迴圈的迴圈體。

  ⑵ 計算基本語句的執行次數的數量級;

  只需計算基本語句執行次數的數量級,這就意味著只要保證基本語句執行次數的函式中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的係數。這樣能夠簡化演算法分析,並且使注意力集中在最重要的一點上:增長率。

  ⑶ 用大Ο記號表示演算法的時間效能。

  將基本語句執行次數的數量級放入大Ο記號中。
下面是推導大O的方法。

1.用常數1取代執行時間中的所有加法常數。

2.在修改後的執行次數函式中,只保留最髙階項。

3.如果最高階項存在且不是1,則去除與這個項相乘的常數。

簡單的說,就是保留求出次數的最高次冪,並且把係數去掉。 如T(n)=2n^2+n+1 =O(n^2)
舉個例子。
[cpp] view plain copy

#include "stdio.h"  

int main()  
{  
    int i, j, x = 0, sum = 0, n = 100;  /* 執行1次 */  
    for( i = 1; i <= n; i++)    /* 執行n+1次 */  
    {  
        sum = sum + i;               /* 執行n次 */
for( j = 1; j <= n; j++) /* 執行n*(n+1)次 */ { x++; /* 執行n*n次 */ sum = sum + x; /* 執行n*n次 */ } } printf("%d", sum); /* 執行1次 */ }

按照上面推導“大O階”的步驟,我們來看
第一步:“用常數 1 取代執行時間中的所有加法常數”,
則上面的算式變為:執行總次數 =3n^2 + 3n + 1
(直接相加的話,應該是T(n) = 1 + n+1 + n + n*(n+1) + n*n + n*n + 1 = 3n^2 + 3n + 3。現在用常數 1 取代執行時間中的所有加法常數,就是把T(n) = 3n^2 + 3n + 3中的最後一個3改為1. 就得到了 T(n) = 3n^2 + 3n + 1)

第二步:“在修改後的執行次數函式中,只保留最高階項”。
這裡的最高階是 n 的二次方,所以算式變為:執行總次數 = 3n^2

第三步:“如果最高階項存在且不是 1 ,則去除與這個項相乘的常數”。
這裡 n 的二次方不是 1 所以要去除這個項的相乘常數,算式變為:執行總次數 = n^2

因此最後我們得到上面那段程式碼的演算法時間複雜度表示為: O( n^2 )

下面我把常見的演算法時間複雜度以及他們在效率上的高低順序記錄在這裡,使大家對演算法的效率有個直觀的認識。
O(1) 常數階 < O(logn) 對數階 < O(n) 線性階 < O(nlogn) < O(n^2) 平方階 < O(n^3) < { O(2^n) < O(n!) < O(n^n) }

最後三項用大括號把他們括起來是想要告訴大家,如果日後大家設計的演算法推匯出的“大O階”是大括號中的這幾位,那麼趁早放棄這個演算法,在去研究新的演算法出來吧。因為大括號中的這幾位即便是在 n 的規模比較小的情況下仍然要耗費大量的時間,演算法的時間複雜度大的離譜,基本上就是“不可用狀態”。

下面通過幾個例子具體分析下時間複雜度計算過程。
一。計算 1 + 2 + 3 + 4 + …… + 100。
常規演算法,程式碼如下:
[cpp] view plain copy

#include "stdio.h"  

int main()  
{  
    int i, sum = 0, n = 100;    /* 執行1次 */  
    for( i = 1; i <= n; i++) /* 執行 n+1 次 */  
    {  
        sum = sum + i;          /* 執行n次 */  
        //printf("%d \n", sum);  
    }  
    printf("%d", sum);          /* 執行1次 */  
}  

從程式碼附加的註釋可以看到所有程式碼都執行了多少次。那麼這寫程式碼語句執行次數的總和就可以理解為是該演算法計算出結果所需要的時間。該演算法所用的時間(演算法語句執行的總次數)為: 1 + ( n + 1 ) + n + 1 = 2n + 3

而當 n 不斷增大,比如我們這次所要計算的不是 1 + 2 + 3 + 4 + …… + 100 = ? 而是 1 + 2 + 3 + 4 + …… + n = ?其中 n 是一個十分大的數字,那麼由此可見,上述演算法的執行總次數(所需時間)會隨著 n 的增大而增加,但是在 for 迴圈以外的語句並不受 n 的規模影響(永遠都只執行一次)。所以我們可以將上述演算法的執行總次數簡單的記做: 2n 或者簡記 n
這樣我們就得到了我們設計的演算法的時間複雜度,我們把它記作: O(n)

然後下面就是我看了很多本演算法的書,上面絕對會提到的一位大神高斯小哥哥的事蹟,我們來看看高斯的演算法,我每次都喊Gauss-Algorithm,程式碼如下:

#include "stdio.h"  

int main()  
{  
    int sum = 0, n = 100;   /* 執行1次 */  
    sum = (1 + n) * n/2;    /* 執行1次 */  

    printf("%d", sum);      /* 執行1次 */  
}  

這個演算法的時間複雜度: O(3),但一般記作 O(1)。
從感官上我們就不難看出,從演算法的效率上看,O(1) < O(n) 的,所以高斯的演算法更快,更優秀。
這就是為什麼很多演算法書上都能看到高斯的影子,沒辦法,人家小學就間接的拿出來了等差數列的前n和表示式。而我呢,大二了,還坐在圖書館敲部落格整理他小時候玩的東西。好了….不散扯了。

二。求兩個n階方陣C=A*B的乘積其演算法如下:
這個矩陣乘法應該是打演算法競賽時經常用到的了,但是時間複雜度。。。。所以很容易Time Limited Error.
但是平常的一些程式設計中經常用到,時間複雜度分析如下。

//右邊列為語句執行的頻度  

   void MatrixMultiply(int A[n][n],int B [n][n],int C[n][n])  

   {  

for(int i=0; i <n; i++)                       //n+1  

      {  

      for (j=0;j < n; j++)                       //n*(n+1)  

           {  

           C[i][j]=0;                                  //n^2  

          for (k=0; k<n; k++)                 //n^2*(n+1)  

               {  

              C[i][j]=C[i][j]+A[i][k]*B[k][j]; //n^3  

              }  

          }  

      }  

  }  

則該演算法所有語句的頻度之和為:

T(n) = 2n^3+3n^2+2n+1; 利用大O表示法,該演算法的時間複雜度為O(n^3)。

void test_(int n)  
{  
    i = 1, k = 100;  
    while (i<n)  
    {  
        k = k + 1;  
        i += 2;  
    }  
}  

設for迴圈語句執行次數為T(n),則 i = 2T(n) + 1 <= n - 1, 即T(n) <= n/2 - 1 = O(n)

分析下列時間複雜度
這題要注意,時間複雜度是O(n^1/2);
和之前李紅老師佈置的題目裡面的第三小題是一種型別的題目
他的迴圈條件不是我們之前看的固定的,而是隨著迴圈語句的改變而改變的
我們設while的迴圈次數為T(n)
那麼迴圈語句裡面的基本語句執行:
//s

void test_3(int n)  
{  
    int i = 0, s = 0;  
    while (s<n)  
    {  
        i++;  
        s = s + i;  
    }  
}  

和第三小問的O(log3n)是一個道理,只是這題根據之前我們說的原則只保留最高次冪的所以是T^2 然後就是O(n^1/2);
這題剛開始看確實是有點不懂….慢慢就好了。

六。Hanoi(遞迴演算法)時間複雜度分析
我們都知道遞迴函式其實是在棧中執行的,我們如果單看空間複雜度的話,資料規模一旦很大的話就會造成棧溢位,stack overflow.所以在用遞迴的時候要謹慎,反正我是除了用遞迴處理一些階乘問題還有漢諾塔問題之外,我也不怎麼敢用遞迴,mmp,因為不會用也不敢用啊,競賽的時候對時間限制的很緊,用不好遞迴肯定time limited error。然後分析遞迴的時間複雜度也是很重要的,真的很重要,但是有點麻煩,然後拿一個漢諾塔的例子來分析,太難的遞迴我不會寫。所以拿個簡單的來看。

void hanoi(int n, char a, char b, char c)  
{  
    if (n==1)  
    {  
        printf("move %d disk from %c to %c \n", n, a, c);  //執行一次  
    }  
    else  
    {  
        hanoi(n-1, a, c, b);    //遞迴n-1次  
        printf("move %d disk from %c to %c \n", n, a, c);  //執行一次  
        hanoi(n-1, b, a, c);    //遞迴n-1次  
    }  
}  

對於遞迴函式的分析,跟設計遞迴函式一樣,要先考慮基情況(比如hanoi中n==1時候),這樣把一個大問題劃分為多個子問題的求解。
故此上述演算法的時間複雜度的遞迴關係如下:
這個是一個寫出來分段函式 -
昨天java實驗課電腦被同學給弄壞了,今天用的他的電腦,我不太會用,所以就把過程寫紙上拍下來上傳
這裡寫圖片描述
圖片倒了,等我下課了再來弄,我要去上課了,等下上李紅大大的課,好了,寫了一早上的部落格,先這麼多了,這個部落格希望可以幫助到我的朋友們,謝謝,因為時間比較匆促,所以如果有錯誤,請聯絡我,謝謝。去上課了,拜拜…….