咱們能夠把代碼跑一遍,而後經過一些工具來統計、監控就能獲得算法執行的時間和佔用的內存大小。爲何還要作時間、空間複雜度分析呢?這種分析方法能比我實實在在跑一遍獲得的數據更準確嗎?算法
首先,確定的說這種評估算法執行效率的方法是正確的。不少數據結構和算法書籍還給這種方法起了一個名字,叫過後統計法。可是這種統計方法存在必定的侷限性。編程
一、測試結果依賴測試的環境以及數據規模的影響數組
好比,咱們拿一樣一段代碼,再不一樣的機器以及不一樣的數據規模可能會有不一樣的結果。數據結構
二、掌握複雜度分析,將能編寫出性能更優的代碼數據結構和算法
因此咱們須要一個不用具體的測試環境、測試數據來測試,就能夠粗略地估計算法的執行效率的方法,這就是咱們須要的時間、空間複雜度分析方法。函數
複雜度分析提供了一個粗略的分析模型,與實際的性能測試並不衝突,更不會浪費太多時間,重點在於在編程時,要具備這種複雜度分析的思惟,有助於產出效率高的代碼。工具
算法的執行效率,簡單的說就是代碼執行的時間。可是怎麼樣在不運行代碼的狀況下,用「肉眼」獲得一段代碼的執行時間呢?這裏有段很是簡單的代碼,求 1,2,3...n 的累加和。如今來估算一下這段代碼的執行時間。性能
1 function countSum(n) { 2 let sum = 0; 3 console.log(n) 4 for (i = 0; i <= n; ++i) { 5 sum = sum + i; 6 } 7 return sum; 8 }
每行代碼對應的 CPU 執行的個數、執行的時間都不同,因此只是粗略估計,咱們能夠假設每行代碼執行的時間都同樣爲 unit_time。在這個假設的基礎之上,這段代碼的總執行時間是多少呢?測試
第 二、3 行代碼分別須要 1 個 unit_time 的執行時間,第 四、5 行都運行了 n 遍,因此須要 2n * unit_time 的執行時間,因此這段代碼總的執行時間就是 (2n+2) * unit_time。
atom
儘管咱們不知道 unit_time 的具體值,可是經過代碼執行時間的推導過程,咱們能夠獲得一個很是重要的規律,那就是全部代碼的執行時間 T(n) 與代碼的執行次數 f(n) 成正比。
咱們能夠把這個規律總結成一個公式,這個公式就是數據結構書上說的大O表示法。
我來具體解釋一下這個公式:
由於這是一個公式,因此用 f(n) 來表示。公式中的 O,表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。
因此,上面例子中的 T(n) = O(2n+2),這就是大 O 時間複雜度表示法。大 O 時間複雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增加的變化趨勢,因此,也叫做漸進時間複雜度(asymptotic time complexity),簡稱時間複雜度。
分析大O通常的步驟以下:
經過上面三個步驟能夠總結出幾個方法
大 O 這種複雜度表示方法只是表示一種變化趨勢。經過上面的公式咱們會忽略掉公式中的常量、低階、係數,只須要記錄一個最大階的量級。因此咱們在分析一個算法、一段代碼的時間複雜度的時候,也只關注循環執行次數最多的那一段代碼就能夠了。這段核心代碼執行次數的 n 的量級,就是整段要分析代碼的時間複雜度。
若是是很長的一個代碼段,能夠把他們拆分計算時間複雜度,而後再加起來
1 function countSum(n) { 2 let sum_1 = 0; 3 console.log('計算:sum_1') 4 for (let p = 0; p < 100; ++p) { 5 sum_1 = sum_1 + p; 6 } 7 8 let sum_2 = 0; 9 console.log('計算:sum_2') 10 for (let q = 0; q < n; ++q) { 11 sum_2 = sum_2 + q; 12 } 13 14 let sum_3 = 0; 15 console.log('計算:sum_3') 16 for (let i = 0; i <= n; ++i) { 17 j = 1; 18 for (let j = 0; j <= n; ++j) { 19 sum_3 = sum_3 + i * j; 20 } 21 } 22 23 return sum_1 + sum_2 + sum_3; 24 }
這個代碼分爲三部分,分別是求 sum_一、sum_二、sum_3。咱們能夠分別分析每一部分的時間複雜度,而後把相加,再取一個量級最大的做爲整段代碼的複雜度。
第一段的時間複雜度是多少呢?這段代碼循環執行了 100 次,因此是一個常量的執行時間,跟 n 的規模無關。強調一下,即使這段代碼循環 10000 次、100000 次,只要是一個已知的數,跟 n 無關,照樣也是常量級的執行時間。當 n 無限大的時候,就能夠忽略。儘管對代碼的執行時間會有很大影響,可是回到時間複雜度的概念來講,它表示的是一個算法執行效率與數據規模增加的變化趨勢,因此無論常量的執行時間多大,咱們均可以忽略掉。由於它自己對增加趨勢並無影響。
那第二段代碼和第三段代碼的時間複雜度應該很容易分析出來是 O(n) 和 O(n^2)。
綜合這三段代碼的時間複雜度,咱們取其中最大的量級。因此,整段代碼的時間複雜度就爲 O(n^2)。也就是說:總的時間複雜度就等於量級最大的那段代碼的時間複雜度。
假設有一個嵌套的循環,咱們把第一層循環叫T1,那麼T1(n)=O(f(n)),第二層循環叫T2,那麼T2(n)=O(g(n)),總共時間 T(n)=T1(n)*T2(n) = O(f(n))*O(g(n))= O(f(n) * g(n))
假設 T1(n) = O(n),T2(n) = O(n^2),則 T1(n) * T2(n) = O(n^3)。在具體的代碼上,咱們能夠把乘法法則當作是嵌套循環
如上面計算sum_3的代碼段 兩個循環爲O(n^2)。
O(1)
O(1) 只是常量級時間複雜度的一種表示方法,並非指只執行了一行代碼。好比這段代碼,即使有 3 行,它的時間複雜度也是 O(1),而不是 O(3)。
1 const i = 8; 2 const j = 6; 3 const sum = i + j;
只要代碼的執行時間不隨 n 的增大而增加,這樣代碼的時間複雜度咱們都記做 O(1)。或者說,通常狀況下,只要算法中不存在循環語句、遞歸語句,即便有成千上萬行的代碼,其時間複雜度也是Ο(1)。
O(logn)
對數階時間複雜度很是常見,如
1 i=1; 2 while (i <= n) { 3 i = i * 2; 4 }
根聽說的複雜度分析方法,第三行代碼是循環執行次數最多的。因此,咱們只要能計算出這行代碼被執行了多少次,就能知道整段代碼的時間複雜度。從代碼中能夠看出,變量 i 的值從 1 開始取,每循環一次就乘以 2。當大於 n 時,循環結束。實際上變量 i 的取值就是一個等比數列。若是我把它一個一個列出來,就應該是這個樣子的:
因此,咱們只要知道 x 值是多少,就知道這行代碼執行的次數了。經過 2x=n 求解 x 這個問題咱們想高中應該就學過了,我就很少說了。x=log2n,因此,這段代碼的時間複雜度就是 O(log2n)。
O(n)
O(n)級別有個很是顯著的特徵就,它會存在一個循環,且循環的次數是和n相關
1 function countSum (n) { 2 let sum = 0 3 for (let i = 0; i < n; i++) { 4 sum += i 5 } 6 }
O(n^2)
O(n^2)級別的有雙重循環
function countSum (n) { let sum = 0 for (let i = 0; i < n; i++) { sum += i for (let J = 0; J < n; i++) { // do some thing } } }
不是全部的雙重循環都是n^2
1 function countSum (n, m) { 2 let sum = 0 3 for (let i = 0; i < n; i++) { 4 sum += i 5 for (let J = 0; J < m; i++) { 6 // do some thing 7 } 8 } 9 }
這種是由兩個數據規模n、m來決定的時間複雜度,因此是O(n * m),關鍵仍是要分析嵌套的循環跟外面那層循環的關係。
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
排序方法 | 時間複雜度(平均) | 時間複雜度(最壞) | 時間複雜度(最好) | 空間複雜度 | 穩定性 | 複雜性 |
---|---|---|---|---|---|---|
直接插入排序 | O(n2)O(n2) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 穩定 | 簡單 |
希爾排序 | O(nlog2n)O(nlog2n) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 不穩定 | 較複雜 |
直接選擇排序 | O(n2)O(n2) | O(n2)O(n2) | O(n2)O(n2) | O(1)O(1) | 不穩定 | 簡單 |
堆排序 | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(1)O(1) | 不穩定 | 較複雜 |
冒泡排序 | O(n2)O(n2) | O(n2)O(n2) | O(n)O(n) | O(1)O(1) | 穩定 | 簡單 |
快速排序 | O(nlog2n)O(nlog2n) | O(n2)O(n2) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | 不穩定 | 較複雜 |
歸併排序 | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(nlog2n)O(nlog2n) | O(n)O(n) | 穩定 | 較複雜 |
基數排序 | O(d(n+r))O(d(n+r)) | O(d(n+r))O(d(n+r)) | O(d(n+r))O(d(n+r)) | O(n+r)O(n+r) | 穩定 | 較複雜 |
時間複雜度的全稱是漸進時間複雜度,表示算法的執行時間與數據規模之間的增加關係。同理,空間複雜度全稱就是漸進空間複雜度(asymptotic space complexity),表示算法的存儲空間與數據規模之間的增加關係,空間複雜度分析跟時間複雜度相似。
1 function run (n) { 2 let name = 'joel' 3 let step= 2 4 const arr = [] 5 6 for (let i = 0; i < n; i++) { 7 arr.push(i * step) 8 } 9 }
再第4行代碼咱們初始化一個數組,再第7行代碼對這個數組進行push 操做,這個循環是依賴外部的n,因此這個空間複雜度爲 O(n),咱們常見的空間複雜度就是 O(1)、O(n)、O(n2 )