數據結構和算法(第 2 章):複雜度分析

一、複雜度分析

首先要明確一點,數據結構和算法本質是解決“快”和“省”的問題。要描述一個算法的好壞就需要用到複雜度分析了,複雜度分析可分爲如下兩種。

  • 時間複雜度

  • 空間複雜度

時間複雜度就是描述算法的快,空間複雜度則是描述算法的省。一般說的複雜度都是時間複雜度,畢竟現代計算機存儲空間已經不那麼拮据了,時間複雜度是我們重點研究的內容。

二、大 O 複雜度表示法

首先看一段代碼,求從 1~n 的累加之和。

int demo(int n) {

    int i;
    int sum = 0;

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

    return sum;
}

現在就來估算一下這段代碼的執行時間(下面都是以時間複雜度爲例講解,空間複雜度最後再講)。

CPU 的角度來看,每一行代碼都執行着類似的操作讀數據-運算-寫數據。這裏爲了方便計算,假設每行代碼的執行時間都是一樣的,用 t 表示執行一行代碼所需要的時間,n 表示數據規模的大小,T(n) 表示代碼執行的總時間。

那麼這段代碼總執行時間是多少呢?我們來數一下。

首先,函數體內有 5 條語句,第 1、2、5 條語句總共執行了 3 次,所需時間是 3*t;第 3、4 條語句各自執行了 n 次,所需時間是 2*n*t。把這兩個代碼段執行的時間相加,所得到的結果就是這段代碼總共所需的時間。

T(n)=(2n+3)t T(n)=(2n+3)t

通過上述公式可以得到一個規律,T(n) 隨着 n 變大而變大,變小而變小。所以,T(n)n 是成正比的,用數學符號表示就可以寫成。

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

其中 f(n) 是代碼段執行所需的時間之和,O 表示 T(n)f(n) 之間的關係是成正比的。

由公式可得代碼段執行所需的時間可表示爲 T(n)=O(2n+3)T(n)=O(2n+3)。這就是O 時間複雜度表示法。大 O 時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨着數據規模增長的變化趨勢,所以,也叫做漸進時間複雜度簡稱時間複雜度

其實O(2n+3)O(2n+3)並不是最終時間複雜度的表示方式。在實際的複雜度分析中,一般會把公式中的常量係數低階忽略。因爲這三部分並不影響增長趨勢(還記得時間複雜度其實是漸進時間複雜度吧!),所以只需要記錄一個最大量級就可以了,時間複雜度的最終表示方式就是O(n)O(n)

三、複雜度的分析方法

1. 最大量階

int demo(int n) {

    int i;
    int sum = 0;

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

    return sum;
}

在分析一個算法、一段代碼的時間複雜度的時候,只關注循環執行次數最多的那一段代碼即可。

2. 加法法則

int demo(int n) {

    int i;
    int sum = 0;

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

    return sum;
}

如果代碼中存在着不同量級的時間複雜度,總的時間複雜度就等於量級最大的那段代碼的時間複雜度。

3. 乘法法則

int demo(int n) {

    int i;
    int sum = 0;

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

    return sum;
}

如果是嵌套、函數調用、遞歸等操作,只需要將各部分相乘即可。

四、複雜度的量級

  • 常量階:O(1)O(1)

  • 對數階:O(logn)O(\log n)

  • 線性階:O(n)O(n)

  • 線性對數階:O(nlogn)O(n \log n)

  • 平方階:O(n2)O(n^2)

  • 立方階:O(n3)O(n^3)

  • k次方階:O(nk)O(n^k)

  • 指數階:O(2n)O(2^n)

  • 階乘階:O(n!)O(n!)

對於上述不同的量級可以分爲兩類:多項式量級非多項式量級。其中,非多項式量級只有兩個:O(2n)O(2^n)O(n!)O(n!),非多項式也叫做 NP問題。

一般情況下,我們常見的複雜度只有O(1)O(1)O(logn)O(\log n)O(n)O(n)O(nlogn)O(n \log n)O(n2)O(n^2) 這五個,常用的分析方法有最大量階、加法法則、乘法法則這三個。只要把這些掌握,基本上就沒有太大問題了。

五、時間複雜度

我們已經分析了時間複雜度,但是還是有一點兒小問題,比如我們要查找某個元素在長度爲 n 的數組中的下標。如果按照順序遍歷,最理想的情況是第一個就是我們要找的,所以時間複雜度是 O(1);如果最後一個才找到我們要的數據,那麼它的時間複雜度是 O(n)

爲了解決同一段代碼在不同情況下時間複雜度出現量級差異,我們就需要對時間複雜度進一步細化分類,爲了更準確、更全面的描述代碼的時間複雜度,引入了一下 4 個概念。

1. 最好情況時間複雜度

代碼在最理想情況下執行的時間複雜度。

2. 最壞情況時間複雜度

代碼在最壞情況下執行的時間複雜度。

3. 平均情況時間複雜度

上面兩個最好、最壞情況都是小概率事件,平均情況時間複雜度纔是最能代表一個算法的時間複雜度。因爲平均情況時間複雜度需要引入概率進行分析,所以也叫做加權平均時間複雜度

4. 均攤時間複雜度

正常情況下,代碼在執行過程中都處於低階的複雜度,極個別情況會出現高階的複雜度,這是我們就可以將高階的複雜度均攤到每個低階的複雜度上,這種分析使用的是攤還分析法的思想。

其實我們只需要知道時間複雜度就夠了。這四種方法都是對時間複雜度的一些特殊情況的補充,也沒必要花大力氣去研究它,大概知道有這種時間複雜度分類就可以了,如果你自己想學或者有腦殘面試官要問這些,那你就自己去查找資料研究研究,這裏不會展開講解。

六、空間複雜度

前面講解過,時間複雜度是漸進時間複雜度,表示算法的執行時間與數據規模之間的增長關係。那麼空間複雜度就是漸進空間複雜度,表示算法的存儲空間與數據規模之間的增長關係。

看一段代碼,定義一個新數組,賦值後遍歷輸出。

void demo(int n) {

    int i;
    int data[n];

    for(i=0; i<n; i++) {
        data[i] = i * i;
    }

    for(i=0; i<n; i++) {
        printf("%d\n", data[i]);
    }
}

跟時間複雜度分析一樣,函數體內第 1 條語句是常量階,直接忽略;第 2 條語句申請了一個大小爲 nint 類型數組,所以整段代碼的空間複雜度就是O(n)O(n)

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