1. 程式人生 > >【資料結構與演算法】之複雜度分析---第一篇

【資料結構與演算法】之複雜度分析---第一篇

一、首先明確兩個問題:

1、為什麼需要對演算法進行復雜度分析?

實際上一個演算法執行所耗費的時間和空間是無法從理論上準確算出來的,必須在計算機上實際執行才知道,但是我們不可能對每個演算法都先在計算機上執行一遍,再決定採用其中效率最高的那個。所以我們就需要從理論上分析出每種演算法的複雜度,從而去預測其在執行的過程中所需要耗費的資源。這兩種方式正好分別對應著通常度量一個程式執行時間的兩種方法:事後統計法事前分析估演算法。這兩種方法從名字上就可以看出其含義,就不做過多的解釋。

對演算法進行預測分析包括以下方面:

(1)預測演算法所需的資源

            《1》計算時間(CPU消耗)

            《2》記憶體空間(RAM消耗)

            《3》通訊時間(頻寬消耗)

(2)預測演算法的執行時間

              在輸入規模一定時,所執行的基本操作的總數量,即演算法的時間複雜度

2、如何衡量演算法的複雜度?

衡量一個演算法的好壞,我們需要給出一些評定的指標,首先我們能夠想到的就是:時間和記憶體,其實還可以從其他方面去衡量,比如:訪問磁碟的次數、指令的總數量等等。但是我們一般只需要關注時間和記憶體就可以了。

總結以上兩個問題就是:同一問題可以用不同演算法解決,而一個演算法的質量優劣將影響到演算法乃至程式的效率。演算法分析的目的在於選擇合適演算法和改進演算法。一個演算法的評價主要從時間複雜度

空間複雜度來考慮。

下面正式進行演算法複雜度的講解:

二、複雜度的基本概念

首先來看下維基百科中對演算法複雜度的定義:演算法複雜度是指演算法在編寫成可執行程式後,執行時所需要的資源,資源包括時間資源和記憶體資源。所以複雜度分為:時間複雜度和空間複雜度。

1、時間複雜度

1.1  基本概念

演算法的時間複雜度是一個函式,它定量的描述了該演算法的執行時間。這是一個關於代表演算法輸入值的字串的長度的函式。時間複雜度通常用大O符號表示,不包括這個函式的低階項和首項係數。使用大O表示法時,時間複雜度可被稱為是漸進的,它考察當輸入值大小趨近於無窮的時的情況,記作O(f(n))。

需要注意的是時間複雜度的全稱是:漸進時間複雜度,即表示的演算法執行時間與資料規模之間的增長關係。

除了時間複雜度的基本概念外,還需要明確其他幾個有關時間複雜度的概念:

複雜度型別 概念
最好情況時間複雜度 在理想情況下,執行該演算法的時間複雜度。比如需要在一個數組的末尾插入一個數
最壞情況複雜度 在最糟糕的情況下,執行該演算法的時間複雜度。比如要在一個數組的開頭位置插入一個數
平均情況複雜度 要查詢的數A在陣列中的位置有n+1種情況(包含不在陣列中的1種情況)。把每週情況下,需要遍歷的元素的個數累加起來,然後再除以(n+1),就可以得到在此陣列中查詢一個元素的平均時間複雜度
均攤時間複雜度 均攤時間複雜度需要考慮每種情況發生的概率,也就是數學中的期望值。

ps:想更多的瞭解均攤時間複雜度可以閱讀:均攤時間複雜度分析

1.2  求解時間複雜度的具體步驟

(1)找出演算法的基本語句:即執行次數最多的語句,通常為內迴圈體;

(2)計算基本語句執行次數的數量級,只保留最高數量級即可,其他省略;

(3)用大O表示其時間複雜度,即為基本語句執行的數量級

1.3 時間複雜度計算的幾個法則

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

(2)乘法法則:巢狀程式碼的複雜度等於巢狀內外程式碼複雜度的乘積

(3)求和法則:T1(m) + T2(n) = O(f(m)) + g(n))

(4)求積法則:T1(m) * T2(n) = O(f(m) * f(n)),和乘法法則一致

1.4 常見的時間複雜度示例

常見演算法的時間複雜度由:常數階O(1),對數階O({\color{Red} }\log _2{n}),線性階O(n),線性對數階(n\log _2{n}),平方階(n^{2}),立方階(n^{3}),k次方階(n^{k})、指數階(2^{n})以及階乘階(n!)。它們的時間複雜度由小到大依次為:

對於上面這些複雜度量級,我們可以粗略的分為兩類:多項式量級(Polynomial)非多項式量級(Non-Deterministic Polynomial)。其中把O(2^{n})和O(n!)稱為非多項式量級,其他的稱為多項式量級。一般認為多項式量級的演算法才是有效的。

(1)常數階O(1)

// 計算兩個變數的和
int a = 10;
int b = 20;
int sum = a + b;

(2)對數階O({\color{Red} }\log _2{n})

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

對數階O({\color{Red} }\log _3{n})

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

ps:如果對數函式不是很熟悉,可以閱讀:對數函式的概念

上面這段程式碼即求:2^{i} = n,所以即i = {\color{Red} }\log _2{n}3^{i} = n即i = {\color{Red} }\log _3{n}

(3)線性階(O(n))

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

(4)線性對數階(O(n\log _2{n}))

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

其實線性對數階就是一個對數階和一個線性階的之間的乘法。

(5)平方階(O(n^{2}))

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

一般複雜度為平方階的都是有兩層迴圈,所以立方階的最好例子就是巢狀三層迴圈的程式,K次方階的依次類推...

有關指數階(2^{n})和階乘階(n!)的演算法都比較複雜,這裡就不再進行舉例分析,感興趣的可以參考:演算法複雜度分析

2、空間複雜度

空間複雜度的全稱是:漸進空間複雜度,即表示演算法的儲存空間與資料規模之間的增長關係,也是問題規模為n的函式,分析過程和時間複雜度類似。

其類似於時間複雜度的分析,主要用於分析該演算法所耗費的儲存空間。一個演算法在計算機儲存上所佔用的空間主要包括三個方面:演算法本身所佔用的儲存空間,演算法的輸入輸出資料所佔用的儲存空間以及演算法在執行過程中臨時佔用的空間。

演算法的本身身所佔用的儲存空間和演算法的書寫長度成正比,這就要求我們儘量寫出簡短的演算法實現程式;

演算法的輸入輸出資料所佔用的儲存空間是由演算法解決的問題決定的,是通過引數表由呼叫函式傳遞而來的,它不隨演算法的不同而改變;

演算法在執行過程中臨時佔用的儲存空間隨演算法的不同而改變,有的演算法只需要佔用少量的臨時單元,而且不隨問題的規模大小而改變,稱這種演算法為:“就地演算法”,是節省儲存的演算法。但是有的演算法需要佔用的臨時工作單元數與解決問題的規模n有關,它隨n的增大而增大,當n較大時,將會佔用較多的儲存單元,例如:歸併排序和快速排序。

我們平時常見的空間複雜度是:O(1)、O(n)、 O(n^{2}) ,它們之上的高數量級空間複雜度幾乎用不到。

下面給出一個分析空間複雜度的簡單例子

public void print(int n){
    int[] a = new int[n];
    int i = 0;

    for(i = 0; i < n; i++){
        a[i] = i * i;
    }
    
    for(i = n - 1; i >= 0; --i){
        System.out.println(a[i]);
    }
}

空間複雜度分析:第2行程式碼中,申請了一個大小為n的int型別陣列,其空間複雜度為O(n),第3行申請了一個儲存空間用來儲存變數i,它的空間複雜度是常量階的O(1),所以該程式的空間複雜度為O(n)。

參考及推薦:

由於本人水平有限,即便參考了很多大佬的文章,難免還是會出現錯誤的地方,如果有人發現,還請指正!謝謝!