評估算法及算法的時間複雜度

文章導讀

【對於一個給定的算法,通常要評估其正確性和運行效率的高低。算法的正確性評估不在本文範圍之內,本文主要討論從算法的時間複雜度特性去評估算法的優劣。】

 

程序是用來解決問題的,是由多個步驟或過程組成的,這些步驟和過程就是解決問題的算法。

解決一個問題有多種方法,也就有多種算法。每一種算法都可以達到解決問題的目的,但花費的成本和時間不盡相同,從節約成本和時間的角度考慮,需要找出最優算法。

那麼,如何衡量一個算法的好壞呢?

顯然,選用的算法應該是正確的(算法的正確性不在此論述)。除此之外,通常有三個方面的考慮:

(1)算法在執行過程中所消耗的時間;

(2)算法在執行過程中所佔資源的大小,例如,佔用內存空間的大小;

(3)算法的易理解性、易實現性和易驗證性等等。

衡量一個算法的好壞,可以通過前面提出的三個方面進行綜合評估。從多個候選算法中找出運行時間短、資源佔用少、易理解、易實現的算法。然而,現實情況卻不盡人意。往往是,一個看起來很簡便的算法,其運行時間要比一個形式上覆雜的算法慢的多;而一個運行時間較短的算法往往佔用較多的資源。

因此,在不同情況下需要選擇不同的算法。在實時系統中,對系統響應時間要求高,則儘量選用執行時間少的算法;當數據處理量大,而存儲空間較少時,則儘量選用節省空間的算法。本文主要討論算法的時間特性,並給出算法在時間複雜度上的度量指標。

一個算法在執行過程中所消耗的時間取決於下面的因素:

(1)算法所需數據輸入的時間;

(2)算法編譯爲可執行程序的時間;

(3)計算機執行每條指令所需的時間;

(4)算法語句重複執行的次數。

其中(1)依賴於輸入設備的性能,若是脫機輸入,則輸入數據的時間可以忽略不計。(2)(3)取決於計算機本身執行的速度和編譯程序的性能。因此,習慣上將算法語句重複執行的次數作爲算法的時間量度。

例如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// x=x+1執行一次

    // 執行一次

    public void add(int x) {

        x = x + 1;

    }

  

    // x=x+1執行n次

    // 執行n次

    public void madd(int x, int n) {

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

            x = x + 1;

    }

  

    // x=x+1執行n*n次

    // 執行n*n次

    public void loopadd(int x, int n) {

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

            for (int j = 0; j < n; j++)

                x = x + 1;

    }

add函數僅包含一條語句,執行次數爲1次;madd函數包含n次的單重for循環,每次循環都執行x=x+1語句,因此執行次數爲n次;loopadd函數包含n次的雙重循環,在第二層循環執行x=x+1語句,因此執行次數爲n*n次,即n^2次。

上面的例子並沒有把循環本身執行的次數算進去,下面給出2個執行次數計算較爲精確的例子。

blob.png

 

函數SUM執行次數爲2n+3次。

再看下面矩陣相加的例子。

blob.png

 

矩陣相加的執行次數爲2n^2+2n+2。

一般情況下,n爲問題規模(大小)的量度,如數組的長度、矩陣的階、圖中的頂點數等等。

對於前面add函數來說,問題規模量度爲常數(1);對於數組排序問題來說,問題規模量度爲輸入數組的長度(記爲n);對於n階矩陣相加來說,問題規模量度爲矩陣階數的平方(記爲n^2)。

爲了給出算法通用的時間量度,用數學概念來描述算法的執行次數,可以把一個算法中語句的執行次數稱爲語句頻度或時間頻度,記爲T(n)。當問題規模n不斷變化時,時間頻度T(n)也會不斷變化,我們需要評估當n不斷變化時,時間頻度T(n)的變化規律。

若有某個輔助函數f(n),當n趨向於無窮大時,如果T(n)/ f(n)的極限爲不等於零的常數,則認爲T(n)與f(n)是同量級的函數,記作:

T(n) =O(f(n))

O(f(n))稱爲算法的漸進時間複雜度,簡稱時間複雜度。

漸進時間複雜度表示的意義是:

(1)在較複雜的算法中,進行精確分析是非常複雜的;

(2)一般來說,我們並不關心T(n)的精確度量,而只是關心其量級。

T (n) = O(f (n)) 表示存在一個常數C,當n趨於正無窮大時,總有:

T (n) ≤ C * f(n)

上面公式的意思是T(n)在n趨於正無窮大時跟f(n)基本接近,因此完全可以用f(n)來表示T(n)。

O(f (n))通常取執行次數中最高次方或最大指數部分的項。例如:

陣列元素相加爲2n+3 = O(n)

矩陣相加爲2n^2+2n+1 = O(n^2)

矩陣相乘爲2n^3+4n^2+2n+2 = O(n^3)

在各種不同的算法中,若算法語句的執行次數爲常數,則算法的時間複雜度爲O(1),按數量級遞增排列,常見的時間複雜度量有:

(1)O(1):常量階,運行時間爲常量

(2)O(logn):對數階,如二分搜索算法

(3)O(n):線性階,如n個數內找最大值

(4)O(nlogn):對數階,如快速排序算法

(5)O(n^2):平方階,如選擇排序,冒泡排序

(6)O(n^3):立方階,如兩個n階矩陣的乘法運算

(7)O(2^n):指數階,如n個元素集合的所有子集的算法

(8)O(n!):階乘階,如n個元素全部排列的算法

下圖給出了隨着n的變化,不同量級的時間複雜度變化曲線。

blob.png

 

圖1 時間複雜度變化曲線圖

評估算法時間複雜度的具體步驟是:

(1)找出算法中重複執行次數最多的語句的頻度來估算算法的時間複雜度;

(2)保留算法的最高次冪,忽略所有低次冪和高次冪的係數;

(3)將算法執行次數的數量級放入大Ο記號中。

例如,下列三個簡單的程序段:

1

2

3

4

5

(a)x =  x + 1;

(b)for(i = 0;i<n;i++)  x=x+1;

(c)for(i = 0;i<n;i++)

     for(j=0;j<n;j++)

          x=x+1;

在程序段(a)中,語句x=x+1不在任何一個循環體內,則它的時間頻度爲1,其執行時間是個常量;而(b)中,同一語句被重複執行n次,其時間頻度爲n;顯然在(c)中,該語句的頻度爲n^2。由此,這三個程序段的時間複雜度爲O(1)、O(n)、O(n^2)。分別爲常量、線性階和平方階。

對於較爲複雜的算法,可以將它們分隔成容易估算的幾個部分,然後利用O的求和原則得到整個算法的時間複雜度。例如,若算法的兩個部分的時間複雜度分別爲T1(n)=O(f(n))和T2(n)=O(g(n)),則總的時間複雜度爲:

T(n)= T1(n)+ T2(n)=O(max(f(n), g(n)))

然而,很多算法的運行時間不僅依賴於問題的規模,也與處理的數據集有關。例如,有的排序算法對某些原始數據(如自小至大有序),則其時間複雜度爲O(n),而對另一些數據可達O(n^2)。因此,在估算算法的時間複雜度時,均以數據集中最壞的情況來估算。

文章小結

評估算法時間複雜度的要點是:如果一個算法的執行次數是 T(n),那麼只保留最高次項,同時忽略最高項的係數後得到函數 f(n),此時算法的時間複雜度就是 O(f(n))。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章