在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化狀況並肯定T(n)的數量級。算法的時間複雜度,也就是算法的時間量度,基座T(n)=O(f(n))。它表示隨問題規模n的增大,算法執行時間的增加率和f(n)的增加率相同,稱做算法的漸進算法時間複雜度,簡稱爲時間複雜度。其中f(n)是問題規模n的某個函數。算法
通常用大寫O()來表示算法的時間複雜度寫法,一般叫作大O記法。函數
通常狀況下,隨着n的增大,T(n)增加最慢的算法爲最優算法。spa
O(1):常數階.net
O(n):線性階設計
O(n2):平方階code
大O推導法:
用常數1取代運行時間中的全部加法常數
在修改後的運行函數中,只保留最高階項
若是最高階項存在且不是1,則去除與這個項相乘的常數
計算 1 + 2 + 3 + 4 + ...... + 100。代碼以下,以前也有講過:blog
#include "stdio.h"
int main() { int i, sum = 0, n = 100; /* 執行1次 */
for( i = 1; i <= n; i++) /* 執行 n+1 次 */ { sum = sum + i; /* 執行n次 */
//printf("%d \n", sum);
} printf("%d", sum); /* 執行1次 */ }
從代碼附加的註釋能夠看到全部代碼都執行了多少次。那麼這寫代碼語句執行次數的總和就能夠理解爲是該算法計算出結果所須要的時間。該算法所用的時間(算法語句執行的總次數)爲: 1 + ( n + 1 ) + n + 1 = 2n + 3get
而當 n 不斷增大,好比咱們此次所要計算的不是 1 + 2 + 3 + 4 + ...... + 100 = ? 而是 1 + 2 + 3 + 4 + ...... + n = ?其中 n 是一個十分大的數字,那麼因而可知,上述算法的執行總次數(所需時間)會隨着 n 的增大而增長,可是在 for 循環之外的語句並不受 n 的規模影響(永遠都只執行一次)。因此咱們能夠將上述算法的執行總次數簡單的記作: 2n 或者簡記 n數學
這樣咱們就獲得了咱們設計的算法的時間複雜度,咱們把它記做: O(n)it
再來看看高斯的算法:
#include "stdio.h"
int main() { int sum = 0, n = 100; /* 執行1次 */ sum = (1 + n) * n/2; /* 執行1次 */ printf("%d", sum); /* 執行1次 */ }
這個算法的時間複雜度: O(3),但通常記做 O(1)。
從感官上咱們就不難看出,從算法的效率上看,O(3) < O(n) 的,因此高斯的算法更快,更優秀。
常數階:
int sum = 0 ; n = 100; /*執行一次*/ sum = (1+n)*n/2; /*執行一次*/ printf("%d",sum); /*執行一次*/
這個算法的運行次數f(n) = 3,根據推導大O階的方法,第一步是將3改成1,在保留最高階項是,它沒有最高階項,所以這個算法的時間複雜度爲O(1);
另外,
int sum = 0 ; n = 100; /*執行一次*/ sum = (1+n)*n/2; /*執行第1次*/ sum = (1+n)*n/2; /*執行第2次*/ sum = (1+n)*n/2; /*執行第3次*/ sum = (1+n)*n/2; /*執行第4次*/ sum = (1+n)*n/2; /*執行第5次*/ sum = (1+n)*n/2; /*執行第6次*/ sum = (1+n)*n/2; /*執行第7次*/ sum = (1+n)*n/2; /*執行第8次*/ sum = (1+n)*n/2; /*執行第9次*/ sum = (1+n)*n/2; /*執行第10次*/ printf("%d",sum); /*執行一次*/
上面的兩段代碼中,其實不管n有多少個,本質是是3次和12次的執行差別。這種與問題的大小無關,執行時間恆定的算法,成爲具備O(1)的時間複雜度,又叫作常數階。
注意:無論這個常數是多少,3或12,都不能寫成O(3)、O(12),而都要寫成O(1)
此外,對於分支結構而言,不管真假執行的次數都是恆定不變的,不會隨着n的變大而發生變化,因此單純的分支結構(不在循環結構中),其時間複雜度也是O(1)。
線性階:
線性階的循環結構會複雜一些,要肯定某個算法的階次,須要肯定特定語句或某個語句集運行的次數。所以要分析算法的複雜度,關鍵是要分析循環結構的運行狀況。
int i; for(i = 0 ; i < n ; i++){ /*時間複雜度爲O(1)的程序*/ }
對數階:
int count = 1; while(count < n){ count = count * 2; /*時間複雜度爲O(1)的程序*/ }
由於每次count*2後,距離結束循環更近了。也就是說有多少個2 相乘後大於n,退出循環。
數學公式:2x = n --> x = log2n
所以這個循環的時間複雜度爲O(logn)
平方階:
int i; for(i = 0 ; i < n ; i++){ for(j = 0 ; j < n ; j++){ /*時間複雜度爲O(1)的程序*/ } }
上面的程序中,對於對於內層循環,它的時間複雜度爲O(n),可是它是包含在外層循環中,再循環n次,所以這段代碼的時間複雜度爲O(n2)。
int i;
for(i = 0 ; i < n ; i++){
for(j = 0 ; j < m ; j++){
/*時間複雜度爲O(1)的程序*/
}
}
可是,若是內層循環改爲了m次,時間複雜度就爲O(n*m)
再來看一段程序:
int i;
for(i = 0 ; i < n ; i++){
for(j = i ; j < n ; j++){
/*時間複雜度爲O(1)的程序*/
}
}
注意:上面的內層循環j = i ;而不是0
由於i = 0時,內層循環執行了n次,當i=1時,執行了n-1次……當i=n-1時,執行了1次,因此總的執行次數爲:
n+(n-1)+(n-1)+...+1 = n(n+1)/2 = n2/2 + n/2
根據大O推導方法,保留最高階項,n2/2 ,而後去掉這個項相乘的常數,1/2
所以,這段代碼的時間複雜度爲O(n2)
下面,分析調用函數時的時間複雜度計算方法:
首先,看一段代碼:
int i,j;
void function(int count){
print(count);
}
for(i = 0 ; i < n ; i++){
function (i)
}
函數的時間複雜度是O(1),所以總體的時間複雜度爲O(n)。
假如function是這樣的:
void function(int count){
int j;
for(j = count ; j < n ;j++){
/*時間複雜度爲O(1)的程序*/
}
}
和第一個的不一樣之處在於把嵌套內循環放到了函數中,所以最終的時間複雜度爲O(n2)
再來看一個比價複雜的語句:
n++; /*執行次數爲1*/
function(n); /*執行次數爲n*/
int i,j;
for(i = 0 ; i < n ; i++){ /*執行次數爲nXn*/
function(i);
}
for(i = 0 ; i < n ; i++){ /*執行次數爲n(n+1)/2*/
for(j = i ; j < n ; j++){
/*時間複雜度爲O(1)的程序*/
}
}
它的執行次數f(n) = 1 + n + n2 + n(n+1)/2 + 3/2n2+3/2 n+1,
根據推導大O階的方法,最終它的時間複雜度爲:O(n2)
常見的時間複雜度:
執行次數函數 | 階 | 術語描述 |
12 | O(1) | 常數階 |
2n+3 | O(n) | 線性階 |
3n2+2n+1 | O(n2) | 平方階 |
5log2n+20 | O(log2n) | 對數階 |
2n+3nlog2n+19 | O(nlogn) | nlog2n階 |
6n3+2n2+3n+4 | O(n3) | 立方階 |
2n | O(2n) | 指數階 |
時間複雜度所耗費的時間是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) <O(2n) < O(n!) <O(nn)