1. 程式人生 > >資料結構與演算法之美 課程筆記二 複雜度分析(上)

資料結構與演算法之美 課程筆記二 複雜度分析(上)

資料結構和演算法本身解決的是“快”和“省”的問題,即如何讓程式碼執行得更快,如何讓程式碼更省空間。所以,執行效率是演算法一個非常重要的考量指標。衡量演算法的執行效率最常用的就是時間和空間複雜度分析。

一、為什麼需要複雜度分析?

把程式碼跑一遍,通過統計、監控來得到演算法執行的時間和佔用的記憶體大小,這種做法叫做事後統計法。事後統計法有非常大的侷限性:

1、測試結果非常依賴測試環境。

測試環境中硬體的不同會對測試結果有很大的影響。

2、測試結果受收據規模的影響很大。

對同一個排序演算法,待排序資料的有序度不一樣,排序的執行時間就會有很大的差別。此外,如果測試資料規模過小,測試結果可能無法真實地反映演算法的效能。

所以,需要一個不用具體的測試資料來測試,就可以粗略估算演算法執行效率的方法。這就是時間、空間複雜度分析方法。

二、大O複雜度表示法

如下面的程式碼,我們怎麼來估算一下其執行時間呢?

int cal(int n) {
    int sum = 0;
    int i = 1;
    for (; i <= n; ++i) {
        sum = sum + i;
    }
    return sum;
}

假設每行程式碼執行的時間都一樣,為unit_time,那麼這段程式碼的總執行時間為(2n+2)*unit_time(第2、3行分別需要1個unit_time的執行時間,第4、5行都執行n遍,需要2n*unit_time的執行時間)。

由此看出,所有程式碼的執行時間T(n)與每行程式碼的執行次數成正比。

再看下面這邊程式碼:

int cal(int n) {
    int sum = 0;
    int i = 1; 
    int j = 1;
    for (; i <= n; ++i) {
        j = 1;
        for (; j <= n; ++j) {
            sum = sum + i * j;
        }    
    }
}

在這段程式碼中,第2、3、4行都需要1個unit_time的執行時間,第5、6行程式碼迴圈執行了n遍,需要2n*unit_time的執行時間,第7、8行程式碼迴圈了n^2

遍,需要2n^2*unite_time的執行時間。所以,整段程式碼總的執行時間為T(n)=(2n^2+2n+3)*unit_time。

由此得到:所有程式碼的執行時間T(n)與每行程式碼的執行次數n成正比。用公式表示:

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

其中T(n)代表執行時間,n表示資料規模的大小,f(n)表示每行程式碼執行的次數總和。

第一個例子T(n)=O(2n+2),第二個例子T(n)=O(2n^2+2n+3);這就是大O時間複雜度表示法。它表示程式碼執行時間隨資料規模增長的變化趨勢,也叫做漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。

當n很大時,公式中的低階、常量、係數三部分不左右增長趨勢所以可以忽略,只需要記錄一個最大量級就可以。前面兩段程式碼的時間複雜度可以記為:T(n)=O(n);T(n)=O(n^2)。

三、時間複雜度分析

三個實用分析程式碼的時間複雜度的方法:

1、只關心迴圈執行次數最多的一段程式碼

2、加法法則:總複雜度等於量級最大的那段程式碼的複雜度

int cal(int n) {
    int sum_1 = 0;
    int p = 1;
    for(; p < 100; ++p) {              -----------時間複雜度:O(1)
        sum_1 = sum_1 + p;         
    }

    int sum_2 = 0;
    int q = 1;
    for(; q < n; ++q) {                -----------時間複雜度:O(n)
        sum_2 = sum_2 + q;               
    }

    int sum_3 = 0;
    int i = 1;
    int j = 1;
    for(; i < n; ++i) {
        j = 1;
        for(; j < n; ++j) {             -----------時間複雜度:O(n^2)
            sum_3 = sum_3 + i * j; 
        }
    }

    return sum_1 + sum_2 + sum_3;
}

上面這段程式碼分三個部分,我們可以分別分析每一部分的時間複雜度,然後把它們放在一塊兒,取一個量級最大的作為整段程式碼的複雜度。所以上面這段程式碼的時間複雜度為O(n^2)。

抽象為公式:

如果T1(n) = O(f(n)),T2(n) = O(g(n));那麼T(n)=T1(n)+T2(n)=max(O(f(n)),O(g(n))) = O(max(f(n),g(n)))

3、乘法法則:巢狀程式碼的複雜度等於巢狀內外程式碼複雜度的乘積

int cal(int n) {
    int ret = 0;
    int p = 1;
    for(; i < n; ++i) {              -----------時間複雜度:O(n)
        ret = ret + f(i);         
    }
}

int f(int n) {
    int sum = 0;
    int i = 1;
    for(; i < n; ++i) {                -----------時間複雜度:O(n)
        sum = sum + i;               
    }
    return sum;
}

假設f()只是一個普通的操作,那麼4~6行的時間複雜度T1(n)=O(n)。但f()函式本身不是一個簡單的操作,它的時間複雜度T2(n)=O(n),所以整個cal()函式的時間複雜度T(n)=T1(n)*T2(n)=O(n)*O(n)=O(n^2)。

四、幾種常見時間複雜度例項分析

按數量級遞增:

常數階 O(1)

對數階 O(logn)

線性階 O(n)

線性對數階 O(nlogn)

平方階 O(n^2)、立方階 O(n^3)...k次方階 O(n^k)

指數階 O(2^n)

階乘階 O(n!)

上述複雜度量級可分為兩類:多項式量級和非多項式量級。其中非多項式量級只有兩個:指數階和階乘階。

當資料規模n越來越大時,非多項式量級演算法的執行時間會急劇增加,求解問題的執行時間會無限增長。所以,非多項式時間複雜度的演算法其實是非常低效的演算法。

1、O(1)

O(1)只是常量級時間複雜度的一種表示方法。只要程式碼的執行使勁不隨n的增大而增長,這樣程式碼的時間複雜度都記作O(1)。一般情況下,只要演算法中不存在迴圈語句、遞迴語句,即使有成千上萬行,其時間複雜度也是O(1)。

2、O(logn)、O(nlogn)

i = 1;
while(i <= n) {
    i = i * 2;
}

在這段程式碼中i的值分別為2^02^12^22^3......2^x,當2^x > n時, 迴圈結束。通過2^x=n求解,x=log_2n。所以這段程式碼的時間複雜度為log_2n

i = 1;
while(i <= n) {
    i = i * 3;
}

同理,在這段程式碼中i的值分別為3^03^13^23^3......3^x,當3^x > n時, 迴圈結束。通過3^x=n求解,x=log_3n。所以這段程式碼的時間複雜度為log_3n

實際上,不管是以2為底,還是以3為底,我們可以把所有對數階的時間複雜度都記為O(logn)。這是因為對數之間是可以相互轉換的(log_3n=log_32*log_2n),而在採用大O表示複雜度的時候,可以忽略係數,即O(Cf(n))=O(f(n))。

如果一段程式碼的時間複雜度為O(logn),迴圈執行n遍,那麼總體的時間複雜度就是O(nlogn)。

3、O(m+n)、O(m*n)

int cal(int m, int n) {
    int sum_1 = 0;
    int p = 1;
    for(; p < m; ++p) {              -----------時間複雜度:O(m)
        sum_1 = sum_1 + p;         
    }

    int sum_2 = 0;
    int q = 1;
    for(; q < n; ++q) {              -----------時間複雜度:O(n)
        sum_2 = sum_2 + q;               
    }

    return sum_1 + sum_2;
}

在上面程式碼中,m和n表示兩個資料規模,我們無法事先評估m和n誰的量級大,所以,在表示複雜度時,不能簡單地利用加法規則,省略掉其中一個。由此,上面程式碼的時間複雜度為O(m+n)。

針對這種情況,加法規則需要變更為T1(m)+T2(n)=O(f(m)+g(n))。但乘法法則繼續有效:T1(m)*T2(n)=O(f(m)*g(n))。

五、空間複雜度分析

空間複雜度全稱漸進空間複雜度(asymptotic space complexity),表示演算法的儲存空間與資料規模之間的增長關係。

void print(int n) {
    int i = 0; 
    int[] a = new int[n];
    for (; i < n; i++) {
        a[i] = i * i;    
    }

    for (i = n-1; i >= 0; --i) {
        print out a[i];
    }
}

上面程式碼中。第2行申請了一個儲存變數i,但它是常量階的,可以忽略。第3行申請了一個大小為n的陣列,除此之外,剩下的程式碼都沒有佔用更多的空間,所以整段程式碼的空間複雜度為O(n)。

常見的空間複雜度為O(1)、O(n)、O(n^2)。

極客時間版權所有: https://time.geekbang.org/column/article/40011