演算法的時間複雜度,是剛開始接觸演算法和資料結構時的概念,在真正使用的時候有時候常常忘記它的推導公式。最近準備校招,把二叉樹、排序、查詢等這些經典的演算法複習了一遍,這次把這些都整理成部落格以便以後檢視,複習計劃接近尾聲,這兩天老是不在狀態,學習圖的時候有點暈乎乎,今天反過頭來把時間複雜度的求解法整理一下,還是頗有收穫,以前很多地方自己存在著理解誤差。希望對大家也有所幫助,有不對的地方還請多指教。
在進行演算法分析時,語句總的執行次數T(n)是關於問題規模n的函式,進而分析T(n)隨n的變化情況並確定T(n)的數量級。演算法的時間複雜度,也就是演算法的時間量度,基座T(n)=O(f(n))。它表示隨問題規模n的增大,演算法執行時間的增長率和f(n)的增長率相同,稱作演算法的漸進演算法時間複雜度,簡稱為時間複雜度。其中f(n)是問題規模n的某個函式。
一般用大寫O()來表示演算法的時間複雜度寫法,通常叫做大O記法。
一般情況下,隨著n的增大,T(n)增長最慢的演算法為最優演算法。
O(1):常數階
O(n):線性階
O(n2):平方階
大O推導法:
- 用常數1取代執行時間中的所有加法常數
- 在修改後的執行函式中,只保留最高階項
- 如果最高階項存在且不是1,則去除與這個項相乘的常數
常數階:
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)