總結:相對於實際運行的測試結果,複雜度分析更具備理論依據,能夠在寫代碼的時候提供性能優劣的理論支撐;彌補實際運行測試的「過後諸葛亮」的缺點;因此複雜度分析並不是徹底決定了代碼的運行,而只是做爲一個代碼性能優化的方向。算法
嘗試分析一下下段代碼的「運行時間」性能優化
function sum(n) { let sum = 0 for (let i = 1; i <= n; i++) { sum += i } return sum }
該段代碼總共有7行,其中一、二、五、6被執行一遍,五、7行爲{,三、4行的執行次數和n有關,現假定一行代碼的執行時間爲定值_time,故總執行時間爲(2n + 5) * _time
上段代碼的運行時間和n的大小成正比
按照這個思路再分析一下下段代碼函數
function sum(n) { let sum = 0 for (let i = 1; i <=n; i++) { let j = 1 for (; j <= n; j++) { sum += (i + j) } } return sum }
一、二、9行代碼被執行一遍,把五、6行代碼看做代碼塊x,則三、四、x被執行n遍,在每一遍執行代碼塊x時五、6又分別被執行n遍,故總耗時:(2 n ^ 2 + n + 2) _time
上段代碼的運行時間和n^2成正比性能
咱們有一個前提假設:每行代碼執行一遍的時間是相同的,因此很容易得出一個結論:代碼的執行時間和代碼的執行次數n成正比
抽象成一個公式:T(n) = O(f(n))
第一段代碼:T(n) = O(f(2n + 5))
第二段代碼:T(n) = O(f(2*n^2 + n + 2))
經過公式計算很容易得出結論:第二段代碼理論上更耗時測試
大O時間複雜度表示法並非代碼運行的準確耗時,只是表示代碼耗時隨着數據規模變化而變化的一個趨勢,若是數據規模n趨近於很是大,則
第一段代碼時間公式能夠簡化爲T(n) = O(fn(n))
,
第二段代碼時間公式能夠簡化爲T(n) = O(fn(n^2))
,優化
基於以上簡化,能夠得出時間複雜度分析的幾個簡單參考法則code
經過前面兩段代碼分析,咱們簡化掉了低階、常量、係數,由於這些對於總體趨勢沒有影響,因此在進行復雜度分析的時候只須要關注被循環執行次數最多的代碼。排序
只關注量級最大的那段代碼
以一下一段代碼爲例遞歸
function foo(n) { let i = 0 let sum = 0; for (; i < 10000; i++) { sum += i } let j = 0 for (; j < n; j++) { sum += j } return sum }
分析其複雜度:一、二、三、七、11行代碼執行1遍,四、5行代碼執行10000遍,八、9行代碼執行n遍,
故總時間:T(n) = O(f(2n + 10000 * 2 + 5))
仍然能夠簡化爲T(n) = O(f(n))
即不管常數、係數有多大,當數據規模n趨緊很大時,描述時間變化趨勢的T(n)依然能夠省略掉這些常數、係數內存
嵌套部分複雜度等於內外複雜度之乘積
參考前面代碼段2關於嵌套代碼的複雜度分析
常見的時間複雜度主要有:
常數階 O(1)
對數階 O(logn)
線性階 O(n)
線性對數階 O(nlogn)
平方階 O(n^2)
指數階 O(2^n)
階乘階 O(n!)
後兩種指數階和階乘階成爲非多項式量級,其餘都稱爲多項式量級,這裏可能都聽過國際象棋盤放米粒的故事,這個故事用到的就是指數的威力,因此通常代碼中極少須要指數階和階乘階複雜度的代碼,由於這種代碼的時間趨勢會隨着數據規模的增加極速暴增。
基本上非循環和遞歸的代碼,複雜度都爲O(1)
let i = 0 let j = 1 let sum = i + j
這種代碼的複雜度和數據規模無關
let i = 1 while (i < n) { i*= 2 }
分析:該段代碼包含一個循環,根據法則1,則影響時間複雜度的代碼實際上只有第3行,設執行次數爲x,則有2 * x = n
-> x = log2n
-> T(n) = O(log2n)
-> T(n) = O(log2e * lgn)
-> T(n) = O(lgn)
注:(log2n表示以2爲底n的對數)
單次循環的複雜度通常爲O(n)
let i = 0 let sum = 0 for (; i< n; i++) { sum += i }
若是有多個循環與多個數據規模變量有關,則不能肯定哪一個影響較大,測試複雜度須要具體分析
let i = 0 let j = 0 let sum = 0 for (; i < m; i++) { sum += i } for (; j < n; j++) { sum += j }
複雜度爲O(m + n)
let i = 0 let sum = 0 for (; i < m; i++) { let j = 0 for(; j < n; j++) { sum += j } }
複雜度爲O(m * n)
就是O(n)和O(lgn)的代碼進行一層嵌套
很常見的一個例子就是雙重循環
最多見的是斐波那契數列的算法
function fb(n){ if(n <= 2){ return 1; }else{ return fb(n-1) + fb(n-2); } }
分析:上段代碼執行最多的是第2行,由於每次遞歸調用都會執行這行代碼,同時在前n - 2次的執行中都會執行第5行,每次第5行的執行一定會執行2次第2行,因此第2行的執行次數爲n - 2 個 2的乘積即2^(n - 2),即
時間複雜度爲T(n) = O(2^n)
舒適提示:不要以大於40的參數調用該函數
不經常使用,不作分析
既然時間複雜度表示的是代碼執行時間趨勢和數據規模之間的增加關係,很容易得出,空間複雜度則爲算法的存儲空間與數據規模之間的增加關係
一個程序的空間複雜度是指運行完一個程序所需內存的大小,利用程序的空間複雜度,能夠對程序的運行所須要的內存多少有個預先估計。一個程序執行時除了須要存儲空間和存儲自己所使用的指令、常數、變量和輸入數據外,還須要一些對數據進行操做的工做單元和存儲一些爲現實計算所需信息的輔助空間。程序執行時所需存儲空間包括如下兩部分。
(1)固定部分:這部分空間的大小與輸入/輸出的數據的個數多少、數值無關,主要包括指令空間(即代碼空間)、數據空間(常量、簡單變量)等所佔的空間,這部分屬於靜態空間。
(2)可變空間:這部分空間的主要包括動態分配的空間,以及遞歸棧所需的空間等,這部分的空間大小與算法有關。一個算法所需的存儲空間用f(n)表示。S(n)=O(f(n)),其中n爲問題的規模,S(n)表示空間複雜度。
仍是用斐波那契數列的例子來分析一下該算法的空間複雜度
function fb(n){ if(n <= 2){ return 1; }else{ return fb(n-1) + fb(n-2); } }
咱們知道函數的執行是一個入棧/出棧的過程,當某一個函數被調用的時候,系統會爲其分配內存空間,並將其壓入執行棧,直到該函數執行完畢便將其彈出執行棧,並釋放其佔用的內存空間,對於該遞歸函數,調用棧中最多時的數量爲n - 2個函數調用,故空間複雜度爲O(n)
相較於時間複雜度,空間複雜度比較簡單,並且隨着硬件的提高(內存大小),通常不太過度關注空間的佔用。
複雜度也叫漸進複雜度,包括時間複雜度和空間複雜度,用來分析算法執行效率與數據規模之間的增加關係,能夠粗略地表示,越高階複雜度的算法,執行效率越低。常見的複雜度並很少,從低階到高階有:O(1)、O(lgn)、O(n)、O(nlgn)、O(n2)
TODO: 對常見的排序算法進行時間/空間複雜度分析