為什么想學?
- 如果你是一名業(yè)務開發(fā)工程師,你可能要說,我整天就是做數(shù)據(jù)庫CRUD(增刪改查)又或者你像我一樣是個前端仔,整日最常用的命令就是
npm run dev
,哪里用得到數(shù)據(jù)結(jié)構和算法啊?那還有必要學習復雜度分析嗎? - 是的,對于大部分開發(fā)來說,網(wǎng)上的現(xiàn)有框架已經(jīng)足夠我們平時的開發(fā)了,很多現(xiàn)成的框架,封裝使用方便,拿來就用,還不用太擔心性能的問題。而我們已經(jīng)很少需要自己實現(xiàn)數(shù)據(jù)結(jié)構和算法。
- 不需要自己實現(xiàn),并不代表什么都不需要了解。如果不知道這些類庫背后的原理,不懂得時間、空間復雜度分析,你如何能用好、用對它們?調(diào)用了某個函數(shù)之后,你又該如何評估代碼的性能和資源的消耗呢?而數(shù)據(jù)結(jié)構和算法學習的精髓就是
復雜度分析
為什么需要復雜度分析?
- 之前的我通過代碼跑一遍,通過統(tǒng)計、監(jiān)控,就能得到算法執(zhí)行的時間和占用的內(nèi)存大小。為什么還要做時間、空間復雜度分析呢?
- 測試結(jié)果依賴測試環(huán)境
測試環(huán)境中硬件的不同會對測試結(jié)果有很大的影響。比如,用不同的cpu處理器 i9處理器上的代碼顯然會比i3處理器上的代碼運行的快。 - 測試結(jié)果受數(shù)據(jù)規(guī)模的影響很大。比如測試數(shù)據(jù)規(guī)模太小,測試結(jié)果可能無法真實地反應算法的性能。
- 測試結(jié)果依賴測試環(huán)境
- 我們需要一個不用具體的測試數(shù)據(jù)來測試,就可以粗略地估計算法的執(zhí)行效率的方法。這就是時間、空間復雜度分析方法。
大O復雜度表示法
算法的執(zhí)行效率,其實就是算法代碼執(zhí)行的時間。如何在不運行代碼的情況下,來評估一段代碼的執(zhí)行時間呢?
-
求1,2,3…n的累加和,那么段這代碼的執(zhí)行時間又是多少呢。
var Sum = function(n) { let result = 0 for(let i= 0;i<n;i++){ result = result+i } return result };
可以看到每一行的代碼都執(zhí)行著類似的操作:讀取-運算-記錄。在這里粗略估計,就可以假設每行代碼執(zhí)行的時間都一樣,為
n_time
。第2行代碼需要1個為
n_time
的執(zhí)行時間,第3,4行都運行了n遍,所以需要2nn_time
的執(zhí)行時間,所以這段代碼總的執(zhí)行時間就是(2n+1)n_time
。 -
var Sum = function(n) { let result = 0 for(let i= 0;i<n;i++){ for(let j= 1;j<n;j++){ result = result+i*j } } return result };
我們依舊假設每個語句的執(zhí)行時間是
n_time
。那這段代碼的總執(zhí)行時間T(n)是多少呢?
第2行代碼,每行都需要1個n_time
的執(zhí)行時間,第3行代碼循環(huán)執(zhí)行了n遍,需要n *n_time
的執(zhí)行時間,第4、5行代碼循環(huán)執(zhí)行了n2遍,所以需
要2n2 *n_time
的執(zhí)行時間。所以,整段代碼總的執(zhí)行時間T(n) = (2n2+n+1)*n_time
。通過這兩段代碼執(zhí)行時間的推導過程,我們可以得到一個非常重要的規(guī)律,那就是,所有代碼的執(zhí)行時間T(n)與每行代碼的執(zhí)行次數(shù)n成正比。
我們可以把這個規(guī)律總結(jié)成一個公式。T(n)=Of(n)
T(n)表示代碼執(zhí)行的時間;n表示數(shù)據(jù)規(guī)模大小;f(n)表示每行代碼執(zhí)行的次數(shù)總和;O表示代碼的執(zhí)行時間T(n)與f(n)表達式成正比。第一個例子中的T(n) = O(2n+1),第二個例子中的T(n) = O(2n2+n+1)。這就是大O時間復雜度表示法。大O時間復雜度實際上并不具體表示代碼真正的執(zhí)行時間,而是表示代碼執(zhí)行時間隨數(shù)據(jù)規(guī)模增長的變化趨勢,所以,也叫作
漸進時間復雜度
(asymptotic time complexity),簡稱時間復雜度
。當n很大,甚至趨近于∞時。在公式中的低階、常量、系數(shù)三部分并不能影響他的趨勢,所以都可以忽略。我們只需要記錄一個最大量級就可以,如果用大O表示法表示剛講的那兩段代碼的時間復雜度,就可以記為:T(n) = O(n); T(n) = O(n2)。
時間復雜度分析
-
只關注循環(huán)執(zhí)行次數(shù)最多的一段代碼
最多法則
大O這種復雜度表示方法表示一種變化趨勢。所以,我
們在分析一個算法、一段代碼的時間復雜度的時候,也和大O這種復雜度一樣只關注循環(huán)執(zhí)行次數(shù)最多的那一段代碼就可以了。這段核心代碼執(zhí)行次數(shù)的n的量級,就是整段要分析代碼的時間復雜度。還是上文的代碼,這次我們來分析他的時間復雜度
var Sum = function(n) { let result = 0 for(let i= 0;i<n;i++){ result = result+i } return result };
其中第2行代碼是常量級的執(zhí)行時間,只是聲明了一個變量與n的大小無關,所以對于復雜度并沒有影響。循環(huán)執(zhí)行次數(shù)最多的是第3、4行代碼,這兩行代碼被執(zhí)行了n次,所以總的時間復雜度就是O(n)。
-
總復雜度等于循環(huán)次數(shù)最多的那段復雜度。
加法法則
var Sum = function(n) { let sum1 = 0 for(let i = 0;i<1000;i++){ sum1 = sum1+i } let sum2 = 0 for(let j= 0;j<n;j++){ sum2 = sum2+j } let sum3 = 0 for(let k= 1;k<n;k++){ for(let m = 0;m<n;m++){ sum3 = sum3+k*m } } return sum1+sum2+sum3 }
這個代碼分為三部分,分別是求sum1、sum2、sum3。可以分別分析每一部分的時間復雜度,然后取一個量級最大的作為整段代碼的復雜度。
第一段的時間復雜度是多少呢?這段代碼循環(huán)執(zhí)行了1000次,所以是一個常量的執(zhí)行時間,跟n的規(guī)模無關。因此無論這個這段代碼循環(huán)了多少次只要是一個已知的數(shù)與n無關那么當n趨向∞時就可以忽略。因為它本身對算法執(zhí)行效率與數(shù)據(jù)規(guī)模增長的趨勢并沒有影響。
第二段,第三段代碼的時間復雜度分別是O(n)和O(n2)綜合這三段代碼的時間復雜度,取其中最大的值。整段代碼的時間復雜度就為O(n2)。
抽象成公式T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n)))
遇到嵌套的 for 循環(huán)的時,時間復雜度呢就是內(nèi)外循環(huán)的乘積。
乘法法則
```
var Sum = function(n) {
let result = 0
let func = function(n) {
let func_result = 0
for(let j= 1;j<n;j++){
func_result = func_result +j
}
return func_result
}
for(let i = 1;i<n;i++){
result = result+func(i)
}
return result
}
```
如果func()函數(shù)只是一個普通的操作,那第10~12行的時間復雜度就是,T1(n) = O(n)。但func()函數(shù)本身不是一個簡單的操作,而其本身的時間復雜度是T2(n) =
O(n),所以,整個Sum()函數(shù)的時間復雜度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n2)。
抽象成公式 **T(n)=T1(n)??T2(n)=O(f(n)? g(n))**
幾種常見時間復雜度分析
對于復雜度量級可以分為以下兩類多項式量級和非多項式量級
-
多項式量級
-
O(1)
通常被稱為常量階,他時間復雜度的一種表示方法,并不是指只執(zhí)行了一行代碼。即便有多行,它的時間復雜度也是O(1),一般情況下,只要算法中不存在循環(huán)語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1)。 - ** O(n)** 線性階
- ** O(n2)...O(n?)** 線性階
- ** O(logn)、O(nlogn)**
對數(shù)階,線性對數(shù)階時間復雜度非常常見,例如var Sum = function(n) { let i=1 while (i <= n) { i = i * 2; } return i }
根據(jù)我們前面講的復雜度分析方法,第4行代碼是循環(huán)執(zhí)行次數(shù)最多的。所以,我們只要算出這行代碼被執(zhí)行了多少次,就能知道整段代碼的時間復雜度。
變量i的值從1開始取,每循環(huán)一次就乘以2。當大于n時,循環(huán)結(jié)束。而i的取值就是一個等比數(shù)列2? 21 22 23 ...... 2? = n
通過2x=n求解x可以算出x=log2?,所以,這段代碼的時間復雜度就是O(log2?)
而實際上,不管是以2為底、以3為底,還是以10為底,因為對數(shù)之間可以互相轉(zhuǎn)化,例如
log3? = log32 *log2?
所以O(log3?) = O(C * log2?)
因為C是個常量我們又可以像在大O復雜度中一樣將它忽略掉因此可以把所有對數(shù)階的時間復雜度都記為O(logn)。而如果一段代碼的時間復雜度是O(logn),我們循環(huán)執(zhí)行n遍,時間復雜度就是O(nlogn)了。
- ** O(n)**
-
O(1)
-
非多項式量級
- O(2?)和O(n!)。指數(shù)階,階層階
空間復雜度分析
時間復雜度的全稱是漸進時間復雜度,表示算法的執(zhí)行時間與數(shù)據(jù)規(guī)模之間的增長關系。相似的,漸進空間復雜度
(asymptotic space complexity)簡稱就是空間復雜度
表示算法的存儲空間與數(shù)據(jù)規(guī)模之間的增長關系。
var Sum = function(n) {
let result = []
for(let i= 0;i<n;i++){
result[i] = i
}
return result
};
跟時間復雜度分析一樣,我們可以看到,第2行代碼中,我們聲明了存儲變量result,整段代碼的空間復雜度就是O(n)。
我們常見的空間復雜度就是O(1)、O(n)、O(n2),
最好情況時間復雜度、最壞情況時間復雜度、平均情況時間復雜度
- 是什么?
- 最壞情況時間復雜度:代碼在最理想情況下執(zhí)行的時間復雜度。
- 最好情況時間復雜度:代碼在最壞情況下執(zhí)行的時間復雜度。
- 平均時間復雜度:用代碼在所有情況下執(zhí)行的次數(shù)的加權平均值表示。
- 為什么要引入這幾個概念?
- 同一段代碼在不同情況下時間復雜度會出現(xiàn)量級差異,為了更全面,更準確的描述代碼的時間復雜度。
- 代碼復雜度在不同情況下出現(xiàn)量級差別時才需要區(qū)別這幾種復雜度。大多數(shù)情況下,是不需要區(qū)別分析它們的。
寫在最后
漸進時間,空間復雜度分析為我們提供了一個很好的理論分析的方向,他能夠讓我們對我們的程序或算法有一個大致的認識,復雜度分析能讓我們對不同的算法有了一個“效率”上的感性認識。
漸進式時間,空間復雜度分析僅僅只是一個理論模型,只能大概的分析,不能直接斷定就覺得O(logn)的算法一定優(yōu)于O(n)
所以在實際開發(fā)中,時刻關心理論時間,空間度模型是有助于產(chǎn)出效率高的程序的,同時,而通過文中提供的粗略的分析模型,也不會浪費太多時間,重點在于要具有這種復雜度分析的思維。