計算時間複雜度

極為常見

原則

在計算時間複雜度時,我們會遵循以下原則:

  • 只考慮最糟的情況
  • 最小的上界
  • 忽略常數
  • 只考慮最高次項

接下來我們會一一介紹這些原則。

最糟的情況

接著請考慮一個問題,給你一個長度為 nn 的序列,接著給你 qq 個查詢,每個查詢求序列的某一個連續區間 [l,r][l,r] 的總和。

首先,如果你已經將前面的章節讀熟,那你應該可以寫出這樣的程式碼:

int n;

cin >> n; // 讀序列長度

int a[n + 1]; // 開陣列

for(int i = 1; i <= n; i++) {
    cin >> a[i]; // 讀入序列
}

cin >> q; // 有 q 個查詢

for (int i = 1; i <= q; i++) {
    int l, r, cnt = 0;
    cin >> l >> r; // 一筆查詢他的左右界
    for (int j = l; j <= r; j++) {
        cnt += a[j]; // 加總,從 l 跑到 r
    }
    cout << cnt << endl;
}

那這個時間複雜度是多少呢,我們可以先想如果只有一個查詢,那要從 ll 跑到 rr,最糟的情況就是 ll 接近 11rr 接近 nn,在這個情況中,要做一次查詢會從一個長度為 nn 的序列從頭跑到尾,也就是 O(n)O(n),雖然你可能認為他不會都跑滿,但根據 big-O 的定義這個演算法就是一個查詢 O(n)O(n)

那因為有 qq 個查詢,所以時間複雜度會增加 qq 倍,也就是 O(qn)O(qn)

最小的上界

如下面這份程式碼是計算 nn 個整數的加總,很容易知道 O(n)O(n) 就是這個演算法的時間複雜度,因為他只是對每個數加一次,而 nn 個數就會加 nn 次。

不過這裡要注意的是,這個 O(n)O(n) 是一個最小的上界,也就是說,這個演算法的時間複雜度不會超過 O(n)O(n),但也有可能是 O(n2),O(n!)O(n^2), O(n!) 之類的,不過我們要找的是最小的上界。

int x = 0, y, n;

cin >> n; // 輸入 n

for (int i = 1; i <= n; i++) {
    cin >> y; // 輸入一個數
    x += y; // 加上去
}

cout << x << endl; // 輸出加總

忽略常數

考慮以下這份程式碼:

for (int i = 1; i <= n; i++) {
    cin >> a[i]; // 讀入序列
}

for (int i = 1; i <= n; i++) {
    a[i] *= 2;
}

for (int i = 1; i <= n; i++) {
    a[i] *= 3;
}

思考看看,這個時間複雜度是多少呢?我們可以看到第一個迴圈是 O(n)O(n),第二個迴圈也是 O(n)O(n),第三個迴圈是 O(3n)O(3n),也許你會認為這個是 O(3n)O(3n),但因為 Big-O 不考慮常數(那個乘與 3 倍的常數),所以這個時間複雜度實際上是 O(n)O(n)

只考慮最高次項

接著考慮以下這份程式碼:

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
        cout << "Hello" << endl;
    }
}

for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
        for (int k = 1; k <= n; k++) {
            cout << "Hello" << endl;
        }
    }
}

這個時間複雜度是多少呢?我們可以看到第一個迴圈是 O(n2)O(n^2),第二個迴圈是 O(n3)O(n^3),或許你會認為這個是 O(n2+n3)O(n^2 + n^3),但因為 Big-O 只考慮最高次項,所以這個時間複雜度實際上是 O(n3)O(n^3)