演算法複雜度分析

陌客&發表於2021-09-19

複雜度分析

​ 演算法的複雜度指的是執行該演算法的程式在執行時所需要的時間和空間(記憶體)資源,複雜度分析主要是從時間複雜度空間複雜度兩個層面來考慮。

大O(big O)表示法

​ 在瞭解時間複雜度之前,我們需要知道怎麼用數學符號將它表示出來。

​ 我們知道,一個演算法的執行時間 = 該演算法中每條語句執行時間之和。假設每條語句執行一次所需要的時間為單位時間,那麼一個演算法的執行時間就和演算法中需要執行語句的次數成正比,即是等於所有語句的頻度(執行次數)之和。

​ 用T[n]表示程式碼的執行時間,n表示資料規模的大小,f(n)表示每行程式碼執行的次數總和,演算法執行時間的公式為:
$$
T[n] = O(f(n))
$$
​ O表示的是程式碼執行時間隨資料規模增長的變化趨勢,也叫做漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度. 下面我們看一個具體的例子:

public int GetSum(int n)
{
    int sum = 0;
    int i = 0;
    int j = 0;
    for(; i < n; i++)
    {
        sum += i;
        for(; j < n; j++)
        {
            sum = sum + i * j;
        }
    }
    return sum;
}

​ 在上面例子中,由於已經假設每條語句執行一次所需時間為單位時間,第3,4,5行執了一次,第6,8行分別執行了n次,第9,11行分別執行了n^2次,可以得出
$$
T(n) = O(2n^2 + 2n + 3)
$$
​ 當n的值非常大時,比如n=100000或者更大時,公式中的低階,常數和係數三部分並不左右增長趨勢,因此可以忽略不計,簡單點,我們可以將公式表示為:
$$
T(n) = O(n^2)
$$

很多時候,在求一個演算法的時間複雜度時,我們都會將n看作一個很大的數,因此只要它的高階項,不要低階項,也不要高階的係數,在後面的例子中還會有所體現.

時間複雜度分析

​ 前面我們已經知道了big O表示法的由來以及如何表示,下面我們具體講解如何計算一段程式碼的時間複雜度.我們只需要記住它的一條原則:只要高階項,不要低階項,也不要高階項的係數.

​ 為什麼可以這麼說呢?因為前面已經說過了,big O表示法只是表示一種變化趨勢,當執行次數n變得無窮大時,整個變化趨勢基本上是由高階項所決定的,低階項對它的影響微乎其微.看下面幾個例子

public int cal(int n)
{
    int sum_1 = 0;
    int p = 1;
    for (; p < 100; ++p)
    {
        sum_1 = sum_1 + p;
    }

    int sum_2 = 0;
    int q = 1;
    for (; q < n; ++q)
    {
        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)
        {
            sum_3 = sum_3 + i * j;
        }
    }

    return sum_1 + sum_2 + sum_3;
}

​ 根據上面的法則,我們可以輕易的得出,它的時間複雜度為 O(n^2)

​ 再看一個例子:

public int cal(int n)
{
    int ret = 0;
    int i = 1;
    for (; i < n; ++i)
    {
        ret = ret + f(i);
    }
}

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

​ 函式cal的時間複雜度是多少呢? 第一個for迴圈裡面巢狀了一個函式,巢狀的函式中又有一個for迴圈,因此時間複雜度為O(n^2)

常見時間複雜度分析

​ 下面是常見的幾種複雜度,簡單解釋其中的幾種

  • O(1) : 表示常量級的時間複雜度,並不是說只執行了一條語句,只要執行語句的數量是常量,都可以用它來表示
  • O(logn) : 對數級,在分析複雜度時,不管對數的底是多少,根據數學公式,都可以將其化成底為2的對數,而在big O表示法中,我們忽略係數,所以在對數階時間複雜度的表示方法中,統一為O(logn)

時間複雜度的四種型別

​ 大部分程式碼的複雜度分析按照上述法則分析都足以應付,但對於少部分程式碼,它們的時間複雜度會隨著輸入資料的順序,位置不同而存在量級的差距.在這種情況下,我們才需要使用到最好時間複雜度,最壞時間複雜度,平均時間複雜度,均攤時間複雜度去分析這部分程式碼.

​ 看這個例子,思考一下它的時間複雜度該怎麼表示呢?

public int find(int[] array, int x)
{
    int i = 0;
    int pos = -1;
    for (; i < array.Length; ++i)
    {
        if (array[i] == x)
        {
            pos = i;
            break;
        }
    }
    return pos;
}

​ 程式碼很簡單,遍歷陣列array,檢視是否存在值為x的數字,如果有,返回其下標,否則返回-1.

​ 如果陣列中第一個元素的值等於x,則它的時間複雜度是O(1),如果陣列中不存在值等於x的元素,則它的時間複雜度為O(n),也就是說,在不同情況下,它的複雜度不一樣,因此我們需要分情況進行討論

最好時間複雜度

​ 指的是在理想情況(最好情況)下,執行這段程式碼的時間複雜度,上面例子中,它的最好時間複雜度為O(1).

最壞時間複雜度

​ 指的是在最壞情況下,執行這段程式碼的時間複雜度,上面例子中,它的最壞時間複雜度為O(n).

平均時間複雜度

​ 指的是概率論中的加權平均值,也叫作期望值,所以平均時間複雜度的全稱應該叫加權平均時間複雜度或者期望時間複雜度,它的公式如下。

​ 其中A(n)表示平均時間複雜度,S是規模為n的例項集,例項I∈S的概率為PI,演算法對例項I執行的基本運算次數是tI

​ 上面排序演算法中,有 n+1 種情況(在陣列中有 n 種情況,不在陣列中有 1 種情況),我們分別需要查詢 [公式] 次。假設每種情況出現的概率都一樣,那所有的查詢次數平均下來即為 [公式] ,加權平均和為 [公式] ,根據我們前面時間複雜度的加法原則,我們去掉低階項,去掉係數以後這種情況最終時間複雜度的大小為 [公式]

​ 上面的例子結論雖然是正確的,由於 n + 1 種情況出現的概率不一樣,因此並不能按照上面的方式進行計算。首先我們知道,要查詢一個數,這個數要麼在陣列中,要麼不在陣列中,為了方便理解,我們假設它們的概率都是1/2,如果在陣列中,被遍歷到的概率是1/n (因為有n個位置,每個位置出現的概率都相同), 所以資料被查詢到的概率是1/2 * 1/n1/2n,所以它的平均複雜度的計算過程為:

均攤時間複雜度

​ 均攤時間複雜度(amortized time complexity),它對應的分析方法為攤還分析或者平攤分析。

​ 聽起來與平均時間複雜度有點類似,比較容易弄混,平均複雜度只在某些特殊情況下才會用到,而均攤時間複雜度應用的場景比它更加特殊、更加有限。

​ 對一個資料結構進行一組連續操作中,大部分情況下時間複雜度都很低,只有個別情況下時間複雜度比較高,而且這些操作之間存在前後連貫的時序關係,這個時候,我們就可以將這一組操作放在一塊兒分析,看是否能將較高時間複雜度那次操作的耗時,平攤到其他那些時間複雜度比較低的操作上。而且,在能夠應用均攤時間複雜度分析的場合,一般均攤時間複雜度就等於最好情況時間複雜度。

空間複雜度分析

​ 時間複雜度的全稱是漸進時間複雜度,表示演算法的執行時間與資料規模之間的增長關係。類比一下,空間複雜度全稱就是漸進空間複雜度(asymptotic space complexity),表示演算法的儲存空間與資料規模之間的增長關係。它的分析規則與時間複雜度一樣,也是只要高階項,不要低階項,也不要高階項的係數,看下面的例子:

public 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)
    {
        Console.WriteLine(a[i]);
    }
}

​ 顯而易見,其忽略低階項和常數項,其空間複雜度為 O(n)

總結

相關文章