算法時間複雜度的計算:從幾道題目講起

引子

最近再來回顧一下算法相關的知識,那自然,首先要學習的就是 時間複雜度的概念,以及其計算方式。下面,我就會簡單地介紹下時間複雜度,以及會給出幾道典型的時間複雜度計算題。

時間複雜度

將算法中基本操作的執行次數作爲算法時間複雜度

常見時間複雜度的大小比較關係:

下面,我將給出幾個簡單的算法,並計算其時間複雜度。

算法案例

案例一

public void fun(int n) {
    int i = 1, j = 100;
    while (i < n) {
        ++j;
        i += 2;
    }
}

求上述算法的時間複雜度。

這個算法很簡單,顯然問題的規模爲n,基本語句爲 i += 2;。我們假設循環進行了 m 次後停止。此時,我們可以得到:

1 + 2m + K = n。其中K是一個常量,可能爲0,也可能爲1,用於修正結果值。因爲在循環結束時,i 可能等於 n,也可能等於 n-1。

上面式子最終解得:m = \frac{n - k-1}{2}

也就是說,上述算法的時間複雜度 T(n) = O(n)

案例二

public void fun(int n) {
    int i, j, x = 0;
    for (i = 1; i < n; ++i) {
        for (j = i + 1; j <= n; ++j) {
            ++x;
        }
    }
}

這個算法也很簡單,算法的規模爲n,基本語句爲 ++x;。簡單的分析下,我們很容易得到:

對於每一個符合條件的 i,++x; 執行的次數爲 n - (i + 1) + 1 = n - i 次。

則 ++x; 執行的總次數爲:\sum_{i=1}^{n-1}(n-i) = 1 + 2+3+...+(n-1)=\frac{(n-1)}{2}(1+n-1)=\frac{(n-1)}{2}n

顯而易見,上述算法的時間複雜度T(n) = O(n^{2})

案例三

public void fun(int n) {
    int i = 0, s = 0;
    while (s < n) {
        ++i;
        s += i;
    }
}

上述算法問題的規模爲n,基本語句爲 ++i;s += i; 兩句。

對於這個問題,我假設 循環經過 m 次結束,s的值爲S(m)。則我們很容易得到:

  • 當 m = 1 時,S(1) = 1;
  • 當 m = 2 時,S(2) = S(1) + 2 = 1 + 2;
  • 當 m = 3 時,S(3) = S(2) + 3 = 1 + 2 + 3;
  • 由上可得:S(m) = 1 + 2 + 3 + ... + m = \frac{m}{2}(m+1)

循環經過m次後停止,此時有 S(m) + K = n。K用於修正結果。即:\frac{m}{2}(m+1) + K = n

我們將其解出,得到結果:

x_{1}=\frac{-1+\sqrt{8n-8K+1}}{2}; x_{2}=\frac{-1-\sqrt{8n-8K+1}}{2}(abandon)

上面的x_{2}是個錯誤值,我們直接將其捨棄。所以,該算法的基本語句執行的次數爲:

f(n)=\sqrt{8n-8K+1}-1

顯而易見,該算法的時間複雜度 T(n) = O(\sqrt{n})

案例四

public void mergeSort(int i, int j) {
    int m = 0;
    if (i != j) {
        m = (i + j) / 2;
        mergeSort(i, m);
        mergeSort(m + 1, j);
    }
    merge(i, j, m);
}

已知下面的條件:

  • 調用該方法時,是通過 mergeSort(1, n) 來調用該方法的。
  • merge() 方法的時間複雜度爲 O(n)。

求 該算法的時間複雜度。

首先,我們來理解一下該方法。該方法實際的邏輯可以理解是,將需要排序的集合二等分爲兩份,分別進行排序。

比如,假設我們給定一個例子,i = 1,j = 20,意味着我們將對一個含有20個元素的集合進行排序。在 mergeSort() 方法中,會將該集合分爲兩個集合,第一個集合是 下標從1到10,第二個集合是下標從 11到20。所以,如果我們設定 mergeSort() 方法的基本操作次數爲 f(n),則 mergeSort() 方法內部的 mergeSort() 方法的基本操作次數就是 f(\frac{n}{2})

有了上面的理解,我們就能夠進行推理:

已知 merge() 方法的時間複雜度爲 O(n),我們假設 merge() 方法的基本操作次數爲 a·n。

我們假設mergeSort()方法的基本操作次數爲 f(n)。我們可以得出:

f(n)=2f(\frac{n}{2})+a\cdot n   ①;

當 n = \frac{n}{2} 時,代入①式有:

f(\frac{n}{2})=2f(\frac{n}{4})+ \frac{a}{2}n    ②;

將 ② 式帶入 ① 式,得到:

f(n)=4f(\frac{n}{4})+ 2a\cdot n   ③;

又有當 n = \frac{n}{4}時,代入①式有:

f(\frac{n}{4})=2f(\frac{n}{8})+ \frac{a}{4}n       ④;

將 ④ 式帶入 ③ 式,得到:

f(n)=8f(\frac{n}{8})+ 3a\cdot n    ⑤;

同樣,我們分別再求f(\frac{n}{8})、...、f(\frac{n}{2^{k}}),綜合上面①、③、⑤式,可以得到:

f(n) =2f(\frac{n}{2})+a\cdot n \\ =4f(\frac{n}{4})+ 2a\cdot n\\ =8f(\frac{n}{8})+ 3a\cdot n\\ ...\\ =2^{k}f(\frac{n}{2^{k}})+ ka \cdot n

也就是說,f(n) =2^{k}f(\frac{n}{2^{k}})+ ka \cdot n      ⑥。

然後,從 mergeSort() 方法我們可以得出  f(1) = O(1)     ⑦  。

這裏,根據 ⑦ 式,我們想辦法化掉 f(\frac{n}{2^{k}}),則當 n = 2^{k} 時,有

k = \log_2n,\\ f(2^{k}) =2^{k}f(1)+ ka \cdot n

這裏,兩個式子結合,替換掉k,則可以得到mergeSort()方法的基本操作次數爲:

f(n) =O(1)\cdot n+ a \cdot n \log_2n

顯然,mergeSort()方法的時間複雜度 T(n) = n \log_2n

案例五

具有 n 個元素的順序表,如上圖,分析其插入和刪除一個元素的時間複雜度。

對於上面這個順序表,假設其是一個數組。我們分析其插入一個元素時,需要移動元素的平均個數。這裏,我們分兩步進行分析:

首先,求概率。

總有 n 個元素,則其總共有 n + 1 個插入點。每個位置被插入的可能性相同,則每一個未知被插入概率爲:P =\frac{1}{n+1}

然後,求元素移動個數。

假設要把新元素插入到第 i 個元素之後(如果在1號元素之前插入,則記做在 0 號元素之後插入),則需要將第 i 號元素之後的所有元素向後移動1位,移動元素的個數爲:n - i 

所以,移動元素個數的期望爲:

E = p\sum_{i=0}^{n}(n-i) \quad = \quad \frac{1}{n+1}\cdot \frac{n+1}{2}n \quad = \quad \frac{n}{2}

顯然,平均要移動一半的元素,插入算法的時間複雜度T(n) = O(n),刪除算法也是一樣爲O(n)。

總結

算法是程序員的內功心法,而時間複雜度又是算法學習的基石。時間複雜度的計算牽涉到的數學知識很多,準備最近趕緊複習一下數學了~

參考文章

1、《數據結構高分筆記》2016版,率輝 主編。

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