複雜度分析是整個算法學習的精髓,只要掌握了它,數據結構和算法的內容基本上就掌握了一半了。git
數據結構和算法解決是 「如何讓計算機更快時間、更省空間的解決問題」。github
所以需從執行時間和佔用空間兩個維度來評估數據結構和算法的性能。算法
分別用時間複雜度和空間複雜度兩個概念來描述性能問題,兩者統稱爲複雜度。數組
複雜度描述的是算法執行時間(或佔用空間)與數據規模的增加關係。數據結構
和性能測試相比,複雜度分析有不依賴執行環境、成本低、效率高、易操做、指導性強的特色。數據結構和算法
掌握複雜度分析,將能編寫出性能更優的代碼,有利於下降系統開發和維護成本。函數
算法的執行時間與每行代碼的執行次數成正比,用 T(n) = O(f(n)) 表示,其中 T(n) 表示算法執行總時間,f(n) 表示每行代碼執行總次數,而 n 每每表示數據的規模。這就是大 O 時間複雜度表示法。性能
1)定義學習
算法的時間複雜度,也就是算法的時間量度。測試
大 O 時間複雜度表示法 實際上並不具體表示代碼真正的執行時間,而是表示 代碼執行時間隨數據規模增加的變化趨勢,因此也叫 漸進時間複雜度,簡稱 時間複雜度(asymptotic time complexity)。
例子1:
function aFun() { console.log("Hello, World!"); // 須要執行 1 次 return 0; // 須要執行 1 次 }
那麼這個方法須要執行 2 次運算。
例子 2:
function bFun(n) { for(let i = 0; i < n; i++) { // 須要執行 (n + 1) 次 console.log("Hello, World!"); // 須要執行 n 次 } return 0; // 須要執行 1 次 }
那麼這個方法須要執行 ( n + 1 + n + 1 ) = 2n +2 次運算。
例子 3:
function cal(n) { let sum = 0; // 1 次 let i = 1; // 1 次 let j = 1; // 1 次 for (; i <= n; ++i) { // n 次 j = 1; // n 次 for (; j <= n; ++j) { // n * n ,也便是 n平方次 sum = sum + i * j; // n * n ,也便是 n平方次 } } }
注意,這裏是二層 for 循環,因此第二層執行的是 n * n = n<sup>2</sup> 次,並且這裏的循環是 ++i,和例子 2 的是 i++,是不一樣的,是先加與後加的區別。
那麼這個方法須要執行 ( n<sup>2</sup> + n<sup>2</sup> + n + n + 1 + 1 +1 ) = 2n<sup>2</sup> +2n + 3 。
2)特色
以時間複雜度爲例,因爲 時間複雜度 描述的是算法執行時間與數據規模的 增加變化趨勢,因此 常量、低階、係數 實際上對這種增加趨勢不產生決定性影響,因此在作時間複雜度分析時 忽略 這些項。
因此,上面例子1 的時間複雜度爲 T(n) = O(1),例子2 的時間複雜度爲 T(n) = O(n),例子3 的時間複雜度爲 T(n) = O(n<sup>2</sup>)。
單段代碼看高頻:好比循環。
function cal(n) { let sum = 0; let i = 1; for (; i <= n; ++i) { sum = sum + i; } return sum; }
執行次數最多的是 for 循環及裏面的代碼,執行了 n 次,因此時間複雜度爲 O(n)。
多段代碼取最大:好比一段代碼中有單循環和多重循環,那麼取多重循環的複雜度。
function cal(n) { let sum_1 = 0; let p = 1; for (; p < 100; ++p) { sum_1 = sum_1 + p; } let sum_2 = 0; let q = 1; for (; q < n; ++q) { sum_2 = sum_2 + q; } let sum_3 = 0; let i = 1; let j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } return sum_1 + sum_2 + sum_3; }
上面代碼分爲三部分,分別求 sum_一、sum_二、sum_3 ,主要看循環部分。
第一部分,求 sum_1 ,明確知道執行了 100 次,而和 n 的規模無關,是個常量的執行時間,不能反映增加變化趨勢,因此時間複雜度爲 O(1)。
第二和第三部分,求 sum_2 和 sum_3 ,時間複雜度是和 n 的規模有關的,爲別爲 O(n) 和 O(n<sup>2</sup>)。
因此,取三段代碼的最大量級,上面例子的最終的時間複雜度爲 O(n<sup>2</sup>)。
同理類推,若是有 3 層 for 循環,那麼時間複雜度爲 O(n<sup>3</sup>),4 層就是 O(n<sup>4</sup>)。
因此,總的時間複雜度就等於量級最大的那段代碼的時間複雜度。
嵌套代碼求乘積:好比遞歸、多重循環等。
function cal(n) { let ret = 0; let i = 1; for (; i < n; ++i) { ret = ret + f(i); // 重點爲 f(i) } } function f(n) { let sum = 0; let i = 1; for (; i < n; ++i) { sum = sum + i; } return sum; }
方法 cal 循環裏面調用 f 方法,而 f 方法裏面也有循環。
因此,整個 cal() 函數的時間複雜度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n<sup>2</sup>) 。
function cal(m, n) { let sum_1 = 0; let i = 1; for (; i < m; ++i) { sum_1 = sum_1 + i; } let sum_2 = 0; let j = 1; for (; j < n; ++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2; }
以上代碼也是求和 ,求 sum_1 的數據規模爲 m、求 sum_2 的數據規模爲 n,因此時間複雜度爲 O(m+n)。
公式:T1(m) + T2(n) = O(f(m) + g(n)) 。
function cal(m, n) { let sum_3 = 0; let i = 1; let j = 1; for (; i <= m; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } }
以上代碼也是求和,兩層 for 循環 ,求 sum_3 的數據規模爲 m 和 n,因此時間複雜度爲 O(m*n)。
公式:T1(m) * T2(n) = O(f(m) * g(n)) 。
包括 O(1)(常數階)、O(logn)(對數階)、O(n)(線性階)、O(nlogn)(線性對數階)、O(n<sup>2</sup>) (平方階)、O(n<sup>3</sup>)(立方階)。
除了 O(logn)、O(nlogn) ,其餘的均可從上面的幾個例子中看到。
下面舉例說明 O(logn)(對數階):
let i=1; while (i <= n) { i = i * 2; }
代碼是從 1 開始,每次循環就乘以 2,當大於 n 時,循環結束。
其實就是高中學過的等比數列,i 的取值就是一個等比數列。在數學裏面是這樣子的:
2<sup>0</sup> 2<sup>1</sup> 2<sup>2</sup> ... 2<sup>k</sup> ... 2<sup>x</sup> = n
因此,咱們只要知道 x 值是多少,就知道這行代碼執行的次數了,經過 2x = n 求解 x,數學中求解得 x = log<sub>2</sub>n 。因此上面代碼的時間複雜度爲 O(log<sub>2</sub>n)。
實際上,不論是以 2 爲底、以 3 爲底,仍是以 10 爲底,咱們能夠把全部對數階的時間複雜度都記爲 O(logn)。爲何呢?
由於對數之間是能夠互相轉換的,log3n = log<sub>3</sub>2 * log<sub>2</sub>n,因此 O(log<sub>3</sub>n) = O(C * log<sub>2</sub>n),其中 C=log<sub>3</sub>2 是一個常量。
因爲 時間複雜度 描述的是算法執行時間與數據規模的 增加變化趨勢,因此 常量、低階、係數 實際上對這種增加趨勢不產生決定性影響,因此在作時間複雜度分析時 忽略 這些項。
所以,在對數階時間複雜度的表示方法裏,咱們忽略對數的 「底」,統一表示爲 O(logn)。
下面舉例說明 O(nlogn)(對數階):
function aFun(n){ let i = 1; while (i <= n) { i = i * 2; } return i } function cal(n) { let sum = 0; for (let i = 1; i <= n; ++i) { sum = sum + aFun(n); } return sum; }
aFun 的時間複雜度爲 O(logn),而 cal 的時間複雜度爲 O(n),因此上面代碼的時間複雜度爲 T(n) = T1(logn) * T2(n) = O(logn*n) = O(nlogn) 。
包括 O(2<sup>n</sup>)(指數階)、O(n!)(階乘階)。
O(2<sup>n</sup>)(指數階)例子:
aFunc( n ) { if (n <= 1) { return 1; } else { return aFunc(n - 1) + aFunc(n - 2); } }
參考答案: 顯然運行次數,T(0) = T(1) = 1,同時 T(n) = T(n - 1) + T(n - 2) + 1,這裏的 1 是其中的加法算一次執行。 顯然 T(n) = T(n - 1) + T(n - 2) 是一個斐波那契數列,經過概括證實法能夠證實,當 n >= 1 時 T(n) < (5/3)<sup>n</sup>,同時當 n > 4 時 T(n) >= (3/2)<sup>n</sup>。 因此該方法的時間複雜度能夠表示爲 O((5/3)<sup>n</sup>),簡化後爲 O(2<sup>n</sup>)。 可見這個方法所需的運行時間是以指數的速度增加的。 若是你們感興趣,能夠試下分別用 1,10,100 的輸入大小來測試下算法的運行時間,相信你們會感覺到時間複雜度的無窮魅力。
時間複雜度能夠分爲:
舉例說明:
// n 表示數組 array 的長度 function find(array, n, x) { let i = 0; let pos = -1; for (; i < n; ++i) { if (array[i] == x) { pos = i; break; } } return pos; }
find 函數實現的功能是在一個數組中找到值等於 x 的項,並返回索引值,若是沒找到就返回 -1 。
最好狀況時間複雜度,最壞狀況時間複雜度
若是數組中第一個值就等於 x,那麼時間複雜度爲 O(1),若是數組中不存在變量 x,那咱們就須要把整個數組都遍歷一遍,時間複雜度就成了 O(n)。因此,不一樣的狀況下,這段代碼的時間複雜度是不同的。
因此上面代碼的 最好狀況時間複雜度
爲 O(1),最壞狀況時間複雜度
爲 O(n)。
平均狀況時間複雜度
如何分析平均時間複雜度 ?代碼在不一樣狀況下複雜度出現量級差異,則用代碼全部可能狀況下執行次數的加權平均值表示。
要查找的變量 x 在數組中的位置,有 n+1 種狀況:在數組的 0~n-1 位置中和不在數組中。咱們把每種狀況下,查找須要遍歷的元素個數累加起來,而後再除以 n+1,就能夠獲得須要遍歷的元素個數的平均值,即:
省略掉係數、低階、常量,因此,這個公式簡化以後,獲得的平均時間複雜度
就是 O(n)。
咱們知道,要查找的變量 x,要麼在數組裏,要麼就不在數組裏。這兩種狀況對應的機率統計起來很麻煩,咱們假設在數組中與不在數組中的機率都爲 1/2。另外,要查找的數據出如今 0~n-1 這 n 個位置的機率也是同樣的,爲 1/n。因此,根據機率乘法法則,要查找的數據出如今 0~n-1 中任意位置的機率就是 1/(2n)。
所以,前面的推導過程當中存在的最大問題就是,沒有將各類狀況發生的機率考慮進去。若是咱們把每種狀況發生的機率也考慮進去,那平均時間複雜度的計算過程就變成了這樣:
這個值就是機率論中的 加權平均值,也叫 指望值,因此平均時間複雜度的全稱應該叫 加權平均時間複雜度 或者 指望時間複雜度。
因此,根據上面結論推導出,獲得的 平均時間複雜度
仍然是 O(n)。
均攤時間複雜度
均攤時間複雜度就是一種特殊的平均時間複雜度 (應用場景很是特殊,很是有限,這裏不說)。
經常使用的時間複雜度所耗費的時間從小到大依次是:
O(1) < O(logn) < (n) < O(nlogn) < O(n<sup>2</sup>) < O(n<sup>3</sup>) < O(2<sup>n</sup>) < O(n!) < O(n<sup>n</sup>)
常見的時間複雜度:
時間複雜度的全稱是 漸進時間複雜度,表示 算法的執行時間與數據規模之間的增加關係 。
類比一下,空間複雜度全稱就是 漸進空間複雜度(asymptotic space complexity),表示 算法的存儲空間與數據規模之間的增加關係 。
定義:算法的空間複雜度經過計算算法所需的存儲空間實現,算法的空間複雜度的計算公式記做:S(n) = O(f(n)),其中,n 爲問題的規模,f(n) 爲語句關於 n 所佔存儲空間的函數。
function print(n) { const newArr = []; // 第 2 行 newArr.length = n; // 第 3 行 for (let i = 0; i <n; ++i) { newArr[i] = i * i; } for (let j = n-1; j >= 0; --j) { console.log(newArr[i]) } }
跟時間複雜度分析同樣,咱們能夠看到,第 2 行代碼中,咱們申請了一個空間存儲變量 newArr ,是個空數組。第 3 行把 newArr 的長度修改成 n 的長度的數組,每項的值爲 undefined ,除此以外,剩下的代碼都沒有佔用更多的空間,因此整段代碼的空間複雜度就是 O(n)。
咱們常見的空間複雜度就是 O(1)、O(n)、O(n<sup>2</sup>),像 O(logn)、O(nlogn) 這樣的對數階複雜度平時都用不到。
複雜度分析關鍵在於多練,所謂孰能生巧。
平時咱們在寫代碼時,是用 空間換時間 仍是 時間換空間,能夠根據算法的時間複雜度和空間複雜度來衡量。
若是你以爲本文章或者項目對你有啓發,請給個贊或者 star 吧,點贊是一種美德,謝謝。
筆者文章常更地址:GitHub
參考文章: