1. 程式人生 > >時間負責度O(N)

時間負責度O(N)

我們假設計算機執行一行基礎程式碼需要執行一次運算。

int aFunc(void) {
    printf("Hello, World!\n");      //  需要執行 1 次
    return 0;       // 需要執行 1 次
}

那麼上面這個方法需要執行 2 次運算

int aFunc(int n) {
    for(int i = 0; i<n; i++) {         // 需要執行 (n + 1) 次
        printf("Hello, World!\n");      // 需要執行 n 次
    }
    return 0;       // 需要執行 1 次
}

這個方法需要 (n + 1 + n + 1) = 2n + 2 次運算。
我們把 演算法需要執行的運算次數 用 輸入大小n 的函式 表示,即 T(n) 。

f(n)表示 演算法執行需要的時間的增長速度 f(n) ==速度
例如:f(n) = n^2
O(f(n)) = O(n^2) 時間複雜度為n^2

演算法的時間複雜度,用來度量演算法的執行時間,記作: T(n) = O(f(n))。它表示隨著 輸入大小n 的增大,演算法執行需要的時間的增長速度可以用 f(n) 來描述。

那麼當我們拿到演算法的執行次數函式 T(n) 之後怎麼得到演算法的時間複雜度呢?

1、我們知道常數項對函式的增長速度影響並不大,所以當 T(n) = c,c 為一個常數的時候,我們說這個演算法的時間複雜度為 O(1);如果 T(n) 不等於一個常數項時,直接將常數項省略。

比如 第一個 Hello, World 的例子中 T(n) = 2,所以我們說那個函式(演算法)的時間複雜度為 O(1)。 T(n) = n

  • 29,此時時間複雜度為 O(n)。

2、我們知道高次項對於函式的增長速度的影響是最大的。n^3 的增長速度是遠超 n^2 的,同時 n^2 的增長速度是遠超 n 的。 同時因為要求的精度不高,所以我們直接忽略低此項。

比如 T(n) = n^3 + n^2 + 29,此時時間複雜度為 O(n^3)。

3、因為函式的階數對函式的增長速度的影響是最顯著的,所以我們忽略與最高階相乘的常數。

比如 T(n) = 3n^3,此時時間複雜度為 O(n^3)。

綜合起來:如果一個演算法的執行次數是 T(n),那麼只保留最高次項,同時忽略最高項的係數後得到函式 f(n),此時演算法的時間複雜度就是 O(f(n))。為了方便描述,下文稱此為 大O推導法。

由此可見,由執行次數 T(n) 得到時間複雜度並不困難,很多時候困難的是從演算法通過分析和數學運算得到 T(n)。對此,提供下列四個便利的法則,這些法則都是可以簡單推匯出來的,總結出來以便提高效率。

1、對於一個迴圈,假設迴圈體的時間複雜度為 O(n),迴圈次數為 m,則這個
迴圈的時間複雜度為 O(n×m)。

void aFunc(int n) {
    for(int i = 0; i < n; i++) {         // 迴圈次數為 n
        printf("Hello, World!\n");      // 迴圈體時間複雜度為 O(1)
    }
}

此時時間複雜度為 O(n × 1),即 O(n)。

2、對於多個迴圈,假設迴圈體的時間複雜度為 O(n),各個迴圈的迴圈次數分別是a, b, c…,則這個迴圈的時間複雜度為 O(n×a×b×c…)。分析的時候應該由裡向外分析這些迴圈。

void aFunc(int n) {
    for(int i = 0; i < n; i++) {         // 迴圈次數為 n
        for(int j = 0; j < n; j++) {       // 迴圈次數為 n
            printf("Hello, World!\n");      // 迴圈體時間複雜度為 O(1)
        }
    }
}

此時時間複雜度為 O(n × n × 1),即 O(n^2)。

3、對於順序執行的語句或者演算法,總的時間複雜度等於其中最大的時間複雜度。

void aFunc(int n) {
    // 第一部分時間複雜度為 O(n^2)
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < n; j++) {
            printf("Hello, World!\n");
        }
    }
    // 第二部分時間複雜度為 O(n)
    for(int j = 0; j < n; j++) {
        printf("Hello, World!\n");
    }
}

此時時間複雜度為 max(O(n^2), O(n)),即 O(n^2)。

4、對於條件判斷語句,總的時間複雜度等於其中 時間複雜度最大的路徑 的時間複雜度。

void aFunc(int n) {
    if (n >= 0) {
        // 第一條路徑時間複雜度為 O(n^2)
        for(int i = 0; i < n; i++) {
            for(int j = 0; j < n; j++) {
                printf("輸入資料大於等於零\n");
            }
        }
    } else {
        // 第二條路徑時間複雜度為 O(n)
        for(int j = 0; j < n; j++) {
            printf("輸入資料小於零\n");
        }
    }
}

此時時間複雜度為 max(O(n^2), O(n)),即 O(n^2)。

時間複雜度分析的基本策略是:從內向外分析,從最深層開始分析。如果遇到函式呼叫,要深入函式進行分析。

最後,我們來練習一下

一. 基礎題
求該方法的時間複雜度

void aFunc(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            printf("Hello World\n");
        }
    }
}

參考答案:
當 i = 0 時,內迴圈執行 n 次運算,當 i = 1 時,內迴圈執行 n - 1 次運算……當 i = n - 1 時,內迴圈執行 1 次運算。
所以,執行次數 T(n) = n + (n - 1) + (n - 2)……+ 1 = n(n + 1) / 2 = n^2 / 2 + n / 2。
根據上文說的 大O推導法 可以知道,此時時間複雜度為 O(n^2)。

二. 進階題
求該方法的時間複雜度

void aFunc(int n) {
    for (int i = 2; i < n; i++) {
        i *= 2;
        printf("%i\n", i);
    }
}

參考答案:
假設迴圈次數為 t,則迴圈條件滿足 2^t < n。
可以得出,執行次數t = log(2)(n),即 T(n) = log(2)(n),可見時間複雜度為 O(log(2)(n)),即 O(log n)。

三. 再次進階
求該方法的時間複雜度

long aFunc(int n) {
    if (n <= 1) {
        return 1;
    } else {
        return aFunc(n - 1) + aFunc(n - 2);
    }
}

參考答案:
顯然執行次數,T(0) = T(1) = 1,同時 T(n) = T(n - 1) + T(n - 2) + 1,這裡的 1 是其中的加法算一次執行。
顯然 T(n) = T(n - 1) + T(n - 2) 是一個斐波那契數列,通過歸納證明法可以證明,當 n >= 1 時 T(n) < (5/3)^n,同時當 n > 4 時 T(n) >= (3/2)^n。
所以該方法的時間複雜度可以表示為 O((5/3)^n),簡化後為 O(2^n)。
可見這個方法所需的執行時間是以指數的速度增長的。如果大家感興趣,可以試下分別用 1,10,100 的輸入大小來測試下演算法的執行時間,相信大家會感受到時間複雜度的無窮魅力。