《大話數據結構》一書在一開始也針對算法的時間複雜度進行了說明。這裏的講解就非常明確,言簡意賅,很容易理解。下面通過《大話數據結構》閱讀筆記的方式,通過原因該書的一些簡單的例子和說明來解釋一下算法的時間複雜度和它的計算方法。
首先從基本定義下手,來了解一下什麼是“算法的時間複雜度”,《大話數據結構》一書中對算法的時間複雜度定義如下:
“算法語句總的執行次數 T(n) 是關於問題規模 n 的函數,進而分析 T(n) 隨 n 的變化情況並確定 T(n) 的數量級。算法的時間複雜度,也就是算法的時間度量,記作:T(n) = O(f(n)) 它表示隨問題規模 n 的增大,算法執行時間的增長率和f(n) 的增長率相同,稱作算法的漸進時間複雜度,簡稱爲時間複雜度。其中 f(n) 是問題規模 n 的某個函數。”
光從定義來理解算法的時間複雜度還是比較難的,我們再結合一個簡單的例子來說明。計算 1 + 2 + 3 + 4 + ...... + 100 = ? 這樣的問題想必大家都遇到過,這裏我們通過 C 語言用最簡單的方法實現一下這個問題的算法。
int sum = 0, n = 100; //執行了 1 次
for (int i = 1; i <= n; i++) { //執行了 n + 1 次
sum += i; //執行了 n 次
}
printf(" sum = %d", sum); //執行了 1 次
從代碼附加的註釋可以看到所有代碼都執行了多少次。那麼這寫代碼語句執行次數的總和就可以理解爲是該算法計算出結果所需要的時間。所以說,上述結算 1 + 2 + 3 + 4 + ...... + 100 = ?的算法所用的時間(算法語句執行的總次數)爲 :
1 + ( n + 1 ) + n + 1 = 2n + 3
而當 n 不斷增大,比如我們這次所要計算的不是 1 + 2 + 3 + 4 + ...... + 100 = ? 而是 1 + 2 + 3 + 4 + ...... + n = ?其中 n 是一個十分大的數字,那麼由此可見,上述算法的執行總次數(所需時間)會隨着 n 的增大而增加,但是在 for 循環以外的語句並不受 n 的規模影響(永遠都只執行一次)。所以我們可以將上述算法的執行總次數簡單的記做:
2n 或者簡記 n
這樣我們就得到了我們設計的計算 1 + 2 + 3 + 4 + ...... + 100 = ?的算法的時間複雜度,我們把它記作:
O(n)
對於同一個問題,解法通常是不唯一的。比如 1 + 2 + 3 + 4 + ...... + 100 = ?這個問題,還有其他的不少算法。我們再來看一個數學家高斯解決這個問題的算法(想必大家都很熟悉這個故事)。
SUM = 1 + 2 + 3 + 4 + ...... + 100
SUM = 100 + 99 + 98 + 97 + ...... + 1
SUM + SUM = 2*SUM = 101 + 101 + 101 + .... + 101 正好 100 個 101
SUM = (100*101)/2 = 5050
同樣我們將這個解法翻譯成 C 語言代碼:
int n = 100, sum = 0; //執行 1 次
sum = (n*(n + 1))/2; //執行 1 次
printf("sum = %d", sum); //執行 1 次
這樣我們針對同一個 1 + 2 + 3 + 4 + ...... + 100 = ?問題,不同的算法又的到了一個算法的時間複雜度:
O(3) 一般記作 O(1) 我們後續給出原因。
從感官上我們就不難看出,從算法的效率上看,O(3) < O(n) 的,所以高斯的算法更快,更優秀(是最優秀的嗎?)。
這種用個大寫的 O 來代表算法的時間複雜度的記法有個專業的名字叫“大O階”記法。那麼通過對上述的例子進行總結,我們給出算法的時間複雜度(大O階)的計算方法。
推導“大O階”的步驟:
1、用常數 1 取代運行時間中的所有加法常數。
2、在修改後的運行次數函數中,只保留最高階項。
3、如果最高階項存在且不是 1 ,則去除與這個項相乘的常數。
下面我們在通過一個有不少 for 循環的例子按照上面給出的推導“大O階”的方法來計算一下算法的時間複雜度。先看一下下面的這個例子的代碼,也是用 C 語言寫的,在註釋上我們仍然對執行次數進行說明。
int n = 100000; //執行了 1 次
for (int i = 0; i < n; i++) { //執行了 n + 1 次
for (int j = 0; j < n; j++) { //執行了 n*(n+1) 次
printf("i = %d, j = %d\n", i, j); //執行了 n*n 次
}
}
for (int i = 0; i < n; i++) { //執行了 n + 1 次
printf("i = %d", i); //執行了 n 次
}
printf("Done"); //執行了 1 次
上面的代碼嚴格的說不能稱之爲一個算法,畢竟它很“無聊而且莫名其妙”(畢竟算法是爲了解決問題而設計的嘛),先不論這個“算法”能解決什麼問題,我們看一下它的“大O階”如何推導,還是先計算一下它的執行總次數:
執行總次數 = 1 + (n + 1) + n*(n + 1) + n*n + (n + 1) + 1 = 2n^2 + 3n + 3 這裏 n^2 表示 n 的 2次方。
按照上面推導“大O階”的步驟我們先來第一步:“用常數 1 取代運行時間中的所有加法常數”,則上面的算式變爲:
執行總次數 = 2n^2 + 3n + 1 這裏 n^2 表示 n 的2次方
第二步:“在修改後的運行次數函數中,只保留最高階項”。這裏的最高階是 n 的二次方,所以算式變爲:
執行總次數 = 2n^2 這裏 n^2 表示 n 的2次方
第三步:“如果最高階項存在且不是 1 ,則去除與這個項相乘的常數”。這裏 n 的二次方不是 1 所以要去除這個項的相乘常數,算式變爲:
執行總次數 = n^2 這裏 n^2 表示 n 的2次方
因此最後我們得到上面那段代碼的算法時間複雜度表示爲: O( n^2 ) 這裏 n^2 表示 n 的2次方。
至此,我們對什麼是“算法的時間複雜度”和“算法的時間複雜度”表示法“大O階”的推導方法進行了簡單的說明。當然要想在日後的實際工作中快速準確的推導出各種算法的“大O階”我們還需要進行大量的聯繫,畢竟熟能生巧嘛。最後我們在把常見的算法時間複雜度以及他們在效率上的高低順序記錄在這裏,是大家對算法的效率有個直觀的認識。
O(1) 常數階 < O(logn) 對數階 < O(n) 線性階 < O(nlogn) < O(n^2) 平方階 < O(n^3) < { O(2^n) < O(n!) < O(n^n) }
最後三項我用大括號把他們括起來是想要告訴大家。如果日後大家設計的算法推導出的“大O階”是大括號中的這幾位,那麼趁早放棄這個算法,在去研究新的算法出來吧。因爲大括號中的這幾位即便是在 n 的規模比較小的情況下仍然要耗費大量的時間,算法的時間複雜度大的離譜,基本上就是“不可用狀態”。
當然了,還是要推薦一下《大話數據結構》這本書的。對於數據結構入門來說,這本書相當不錯,很“生動活潑”,讀起來也很有意思!