BlackFlame33

BlackFlame33

若无力驾驭,自由便是负担。个人博客 https://blackflame33.cn/

算法學習筆記-01

複雜度分析淺析

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

可能平常,都会通过统计、监控,来得到相关的性能数据,来评估性能。有人将这种平时使用的性能分析方式,称为:事后分析法

但是事后分析法,虽然普遍存在,但其有一些缺陷:

  1. 测试结果非常依赖测试环境。
    • 某种意义上这也很方便,但是面对不同的机器、不同的编译环境,同段代码可能会有不同的结果。
  2. 测试结果受测试规模的影响很大。
    • 同段代码,在大规模和小规模下会有不一样的结果。

所以,能不能有一种方法,能让我们无视测试环境,在任意规模下都能较精确地分析出代码的性能呢?

自然是有的,那就是使用大 O 複雜度表示法運用時間複雜度空間複雜度來分析代碼

二、大 O 複雜度表示法#

大 O 表示法:算法的时间复杂度通常用大 $O$ 符号表述,定义为 $T (n) = O\big (f (n)\big)$。称函数 $T (n)$ 以 $f (n)$ 为界或者称 $T (n)$ 受限于 $f (n)$。 如果一个问题的规模是 n,解这一问题的某一算法所需要的时间为 $T (n)$。$T (n)$ 称为这一算法的 “时间复杂度”。当输入量 n 逐渐加大时,时间复杂度的极限情形称为算法的 “渐近时间复杂度”。—— 百度百科

時間複雜度#

時間複雜度:全稱 “漸進時間複雜度”,表示算法的執行時間與數據規模之間的增長關係。

我們這裡用一個公式來表示:

T(n)=O(f(n))T(n) = O\big(f(n)\big)
$T(n)$$n$$f(n)$$O$
代碼的執行時間代碼的規模每行代碼的執行次數$T (n)$ 與 $f (n)$ 成正比關係

為了更方便理解這個公式,我舉一個例子:

#include <stdio.h>
int main(){
    int c = 0;
    for(int i=1; i<=n; ++i)
        c += i;

    return 0;
}

從 CPU 的角度來看,這段代碼的每一行都執行著類似的操作:讀數據 - 運算 - 寫數據。儘管每行代碼對應的 CPU 執行的個數、執行的時間都不一樣,但是,我們這裡只是粗略估計,所以可以假設每行代碼執行的時間都一樣,為 unit_time。在這個假設的基礎之上,這段代碼的總執行時間是多少呢?

第 3 行代碼需要 1 個 unit_time 的執行時間,第 4、5 行都運行了 n 遍,所以需要 2n * unit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+1) * unit_time。可以看出,所有代碼的執行時間 T (n) 與每行代碼的執行次數成正比

時間複雜度的分析方法#

1、只關注循環次數最多的一段代碼#

這裡有一條目前我覺得很重要的推斷:由於低階、係數和常量均不影響規模大小,所以 $f (n)$ 取最高階代表即可。

2、加法法則#

總複雜度等於量級最大的那段代碼的複雜度

我們舉一個例子:

#include <stdio.h>
int main(){
    int x=0;
    for(int i=1; i<=100; ++i)
        x += i;

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

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

    printf("%d %d %d\n", x, y, z);
    return 0;
}

在這個例子中,要求 x,y,z。我們分別求出每個部分的時間複雜度,然後取最高階的作整段代碼的複雜度。

求 x 的這段,它會循環 100 次。由於是常數,所以不作數。

求 y 的這段,它會循環 n 次。記 $O (n)$。

求 z 的這段,它會循環 $n^2$ 次。記 $O (n^2)$。

我們從中取最高階 $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)))\because T_1(n)=O\big(f(n)\big), T_2(n)=O\big(g(n)\big)\\\\ \therefore T_總(n)=T_1(n)+T_2(n)=max\Big(O\big(f(n)\big)\Big)\\\\ \\\\ O\big(g(n)\big)=O\Big(max\big(f(n),g(n)\big)\Big)
3、乘法法則#

嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

我們舉個例子:

#include <stdio.h>
int f(int n);
int main(){
    int x = 0;
    for(int i=0;i<=n;++i)
        x += f(i);

    return 0;
}

int f(int n){
    int y = 0;
    for(int i=0;i<=n;++i)
        y += i;

    return y;
}

在這個例子中,如果我們把 $f ()$ 看作一個普通操作,那麼第 6 行的時間複雜度就是 $T_1 (n)=O (n)$。但是 $f ()$ 是一個函數,它的時間複雜度為 $T_2 (n)=O (n)$,所以,這段代碼的時間複雜度應該是,$T_總 = T_1 (n) * T_2 (n)=O (n*n)=O (n^2)$。

幾種常見時間複雜度實例分析#

複雜度量級

其中,我們可以將其分為兩類:多項式量級非多項式量級。非多項式量級只有兩個:$O (2^n)$ 和 $O (n!)$。

我們把時間複雜度為非多項式量級的算法問題叫做 NP 問題。當數據規模 n 越來越大時,非多項式量級算法的執行時間會急劇增加,求解問題的執行時間會無限增長。所以,非多項式時間複雜度的算法其實是非常低效的算法。

1、$O (1)$—— 常量級時間複雜度#
  • 這個‘1’只是一種表示方法。

只要代碼的執行時間不隨 n 的增大而增長,或者,一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間複雜度也是 $O (1)$

2、$O (\log n)$、$O (n\log n)$—— 對數階時間複雜度#
  • 可以把所有對數階的時間複雜度都記作 $O (\log n)$,$O (n\log n)$ 同理。

根據換底公式,對數之間是可以互相轉換的,所以說,任意對數都可以轉換成 $Cf (n)$,其中 $f (n)$ 為對數。而根據之間得到的結論,常數等不影響代碼規模的可以忽略不計。所以在對數階時間複雜度的表示方法裡,我們忽略對數的 “底”,統一表示為 $O (\log n)$

3、$O (m+n)$、$O (m*n)$—— 兩個數據規模的時間複雜度#

一般的,由兩個數據的規模來決定的代碼的複雜度,由於無法事先評估誰的量級大,所以我們在表示複雜度的時候,不能簡單地利用加法法則,省略掉其中一個。所以,我們用 $O (m+n)$、$O (m*n)$ 來表示。

那麼,在這種情況下,加法法則應該為:

T1(m)=O(f(m)),T2(n)=O(g(n))T(n)=T1(m)+T2(n)=O(f(n)+g(n))\because T_1(m)=O\big(f(m)\big), T_2(n)=O(g(n))\\\\ \therefore T_總(n)=T_1(m)+T_2(n)=O\big(f(n)+g(n)\big)

而乘法法則不變。

空間複雜度分析#

空間複雜度,全稱漸進空間複雜度表示算法的存儲空間與數據規模之間的增長關係

我們用一個例子來看一看。

#include <stdio.h>
int main(){
    int a[n];
    for(int i=0; i<=n; ++i)
        a[i] = i * i;
    for(int i=n-1; i>=0; --i)
        pritntf("%d", a[i]);

    return 0;
}

那麼,分析這段代碼,第三行申請了一個大小為 n 的 int 類型數組,除此之外,剩下的代碼都沒有占用更多的空間,所以整段代碼的空間複雜度就是 $O (n)$。

三、內容小結#

複雜度也叫漸進複雜度,包括時間複雜度空間複雜度,用來分析算法執行效率與數據規模之間的增長關係,可以粗略地表示,越高階複雜度的算法,執行效率越低。常見的複雜度並不多,從低階到高階有 $O (1)$、$O (\log n)$、$O (n)$、$O (n\log n)$、$O (n^2)$。

各常見複雜度中 T (n) 與 n 的正比關係

參考文章#

MathJax 基本的使用方式

數據結構與算法之美 —— 王爭

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。