時間複雜度和空間複雜度的計算

1 算法效率的度量方法

事前分析估算方法:在計算機程序編制前,依據統計方法對算法進行估算。

一個程序在計算機上運行時所消耗的時間取決於下列因素:

  • 算法採用的策略、方法

  • 編譯產生的代碼質量

  • 問題的輸入規模

  • 機器執行指令的速度

根據以上因素,拋開與計算機硬件、軟件有關的因素,一個程序的運行時間,依賴於算法的好壞和問題的輸入規模。所謂問題輸入規模是指輸入量的多少。

// 執行了1+(n+1)+n+1=2n+3次
int i, sum = 0, n = 100; // 執行1次
for (i = 1; i <= n; i++) { // 執行了n+1次
	sum = sum + i; // 執行了n次
}
printf("%d", sum); // 執行1次

// 執行了1+1+1=3次
int sum = 0, n = 100; // 執行1次
sum = (1 + n) * n/2; // 執行1次
printf("%d", sum); // 執行1次

int i, j, x = 0, sum = 0, n = 100; // 執行1次
for (i = 1; i <= n; i++) { // 內循環每一次都執行n次,兩個循環執行n x n次
	for (j = 1; j <= n; j++) {
		x++;
		sum = sum + x;
	}
}
printf("%d", sum); // 執行1次

上面的程序,同樣的輸入規模n,求和算法的第一種,求1+2+…+n需要一段代碼運行n次。那麼這個問題的輸入規模使得操作數量是 f(n)=n,顯然運行100次的同一段代碼規模是運算10次的10倍;而第二種,無論n爲多少,運行次數都爲1,即 f(n)=1;第三種,運算100次是運算10次的100倍,因爲它是 f(n)=n²

2 函數的漸進增長

假設有兩個算法,算法A要做2n+3次操作(可以認爲是有兩次n循環,然後三次賦值或打印代碼),算法B要做3n+1次操作,哪種算法更快。

在這裏插入圖片描述

根據上表的數據,輸入規模n在沒有限制的情況下,只要超過一個數值N,這個函數就總是大於另一個函數,我們稱爲函數是漸進增長的。

漸進增長:給定兩個函數f(n)和g(n),如果存在一個整數N,使得對於所有的n>N,f(n)總是比g(n)大,那麼,我們說f(n)的增長漸進快於g(n)

隨着n的增大,後面的算法A是+3還是算法B是+1已經不影響最終的算法變化,所以我們可以忽略這些加法常數。看最終的算法A′和算法B′就可以得出哪種算法優劣。

算法C是4n+8,算法D是2n²+1,哪個算法更快。

在這裏插入圖片描述

根據上表的數據,隨着n的增大,算法C的優勢越來越優於算法D了,即使我們去掉常數發現其實結果沒有發生改變,即算法C變成4n,算法D變成2n²,結果還是算法C優於算法D;甚至我們去掉與n相乘的常數,結果也沒有發生改變,即最終的算法C′和算法D′展示的結果。也就是說,與最高次項相乘的常數並不重要

算法E是2n²+3n+1,算法F是2n³+3n+1,哪個算法更快。

在這裏插入圖片描述

根據上表的數據,可以發現,最高次項的指數大的,函數隨着n的增長,結果也會增長特別快

算法G是2n²,算法H是3n+1,算法I是2n²+3n+1,哪個算法更快。

在這裏插入圖片描述

根據上表的數據,當n的值越來越大時,算法H已經沒法和算法G和算法I相比較了,最終幾乎可以忽略不計,而算法G已經很趨近於算法I。所以可以得出結論,判斷一個算法的效率時,函數中的常數和其他次要項常常可以忽略,而更應該關注主項(最高階項)的階數

3 算法時間複雜度

算法時間複雜度定義:在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進而分析T(n)隨n的變化情況並確定T(n)的數量級。算法的時間複雜度,也就是算法的時間度量,記作:T(n)=O(f(n))。它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸近時間複雜度,簡稱時間複雜度。其中f(n)是問題規模n的某個函數。

用大寫 O() 來體現算法時間複雜度的記法,稱之爲大O記法。

一般情況下,隨着n的增大,T(n)增長最慢的算法爲最優算法。

3.1 推導大O階方法(如何計算時間複雜度)

推導大O階可以通過以下三個步驟推算:

  • 用常數1取代運行時間中的所有加法常數

  • 在修改後的運行次數函數中,只保留最高階項

  • 如果最高階項存在且不是1,則去除與這個項相乘的常數

得到的結果就是大O階。

比如使用上面的算法G是2n²,算法H是3n+1,算法I是2n²+3n+1,經過第一二步驟後,算法G爲2n²,算法H是3n,算法I是2n²,經過第三個步驟後,算法G爲n²,算法H爲n,算法I爲n²,所以就推導出:算法G和算法I的時間複雜度爲 O(n²),算法H的時間複雜度爲 O(n)

3.2 常數階

int sum = 0, n = 100; // 執行1次
sum = (1+n) * n / 2; // 執行1次
printf("%d", sum); // 執行1次

上面的算法運行次數函數是f(n)=3,根據推導大O階的方法,發現它根本沒有最高階項,所以這個算法時間複雜度爲 O(1)

無論添加執行多少次 sum=(1+n) * n / 2,無論n爲多少,總是該句代碼的執行次數,而與n的大小無關,執行時間恆定的短髮,稱之爲具有O(1)時間複雜度,又叫常數階。

3.2 線性階

要確定某個算法的階次常常需要確定某個特定語句或某個語句集的運行次數。因此,我們要分析算法的複雜度,關鍵就是要分析循環結構的運行情況

int i;
for (i = 0; i < n; i++) { // 執行了n次,時間複雜度爲O(n)
	// 時間複雜度爲O(1)的程序步驟序列
}

3.3 對數階

// 每次count*2後距離n更近一分,即有多少個2相乘後大於n則會退出循環
// 2x=n(x爲次方),x = log2n,所以時間複雜度爲O(logn)
int count = 1;
while (count < n) {
	count = count * 2;
	// 時間複雜度爲O(1)的程序步驟序列
}

3.4 平方階

// 外循環執行n次,時間複雜度爲O(n)
// 內循環每次都執行n次,時間複雜度爲O(n)
// 總共執行n²次,整個算法的時間複雜度爲O(n²)
int i, j;
for (i = 0; i < n; i++) {
	for (j = 0; j < n; j++) {
		// 時間複雜度爲O(1)的程序步驟序列
	}
}

// 外循環執行m次,時間複雜度爲O(m)
// 內循環每次執行n次,時間複雜度爲O(n)
// 總共執行了nxm次,時間複雜度爲O(nxm)
for (i = 0; i < m; i++) {
	for (j = 0; j < n; j++) {}
}

// i=0,內循環執行了n次;i=1,內循環執行了n-1次;i=n-1時,執行了1次
// 總執行次數爲n+(n-1)+(n-2)+...+1= n²/2 + n/2
// 根據大O推導,經過第一二步驟後,算法爲n²/2,第三步驟去除和最高階相乘或相除的常數
// 所以該算法的時間複雜度爲O(n²)
for (i = 0; i < n; i++) {
	for (j = i; j < n; j++) {}
}

4 常見的時間複雜度

在這裏插入圖片描述

常用的時間複雜度所耗費的時間從小到大依次是:

在這裏插入圖片描述

在實際的項目中,一般不會去處理立方階之後的算法也不切實際,最常見的就是常數階、線性階、平方階、對數階、nlogn階。

5 最壞情況和平均情況

在算法的分析中,比如查找一個有n個隨機數字數組中的某個數字,最好的情況是第一個數字就是,那麼算法複雜度爲O(1),但也有可能這個數字就在最後一個位置,那麼算法的時間複雜度就是O(n)。

最壞情況運行時間是一種保證,那就是運行時間將不會再壞了。在應用中,這是一種最重要的需求,通常,除非特別指定,我們提到的運行時間都是最壞情況的運行時間

而平均運行時間也就是從概率的角度看,這個數字在每一個位置的可能性是相同的,所以平均的查找時間爲n/2次後發現這個目標元素。

平均運行時間是所有情況中最有意義的,因爲它是期望的運行時間。在實際項目中很難通過分析得到,一般都是通過運行一定數量的實驗數據後估算出來。

6 算法空間複雜度

算法的空間複雜度通過計算算法所需的存儲空間時間,算法空間複雜度的計算公式記作:S(n)=O(f(n)),其中,n爲問題的規模,f(n)爲語句關於n所佔存儲空間的函數。

一般情況下,一個程序在機器上執行時,除了需要存儲程序本身的指令、常數、變量和輸入數據外,還需要存儲對數據操作的存儲單元。若輸入數據所佔空間只取決於問題本身,和算法無關,這樣只需要分析該算法在實現時所需的輔助單元即可。所算法執行時所需的輔助空間相對於輸入數據量而言是個常數,則稱次算法爲原地工作,空間複雜度爲O(1)。

一般我們提到複雜度,都是說的時間複雜度,在實際的工作中,主要的還是會分析時間複雜度。

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