你是怎么理解算法的呢?
簡單說就是,同一個功能
- 別人寫的代碼跑起來占內存 100M,耗時 100 毫秒
- 你寫的代碼跑起來占內存 500M,耗時 1000 毫秒,甚至更多
所以
- 衡量代碼好壞有兩個非常重要的標准就是:
運行時間
和占用空間
,就是我們后面要說到的時間復雜度和空間復雜度,也是學好算法的重要基石
- 這也是會算法和不會算法的攻城獅的區別、更是薪資的區別,因為待遇好的大廠面試基本都有算法
可能有人會問:別人是怎么做到的?代碼沒開發完 運行起來之前怎么知道占多少內存和運行時間呢?
確切的占內用存或運行時間確實算不出來,而且同一段代碼在不同性能的機器上執行的時間也不一樣,可是代碼的基本執行次數,我們是可以算得出來的,這就要說到時間復雜度了
什么是時間復雜度
看個栗子
function foo1(){ console.log("我吃了一顆糖") console.log("我又吃了一顆糖") return "再吃一顆糖" }
調用這個函數,里面總執行次數就是3次,這個沒毛病,都不用算
那么下面這個栗子呢
function foo2(n){ for( let i = 0; i < n; i++){ console.log("我吃了一顆糖") } return "一顆糖" }
那這個函數里面總執行次數呢?根據我們傳進去的值不一樣,執行次數也就不一樣,但是大概次數我們總能知道
let = 0 :執行 1 次
i < n : 執行 n+1 次
i++ : 執行 n+1 次
console.log("執行了") : 執行 n 次
return 1 : 執行 1 次
這個函數的總執行次數就是 3n + 4 次,對吧
可是我們開發不可能都這樣去數,所以根據代碼執行時間的推導過程就有一個規律,也就是所有代碼執行時間 T(n)和代碼的執行次數 f(n) ,這個是成正比的,而這個規律有一個公式
T(n) = O( f(n) )
n 是輸入數據的大小或者輸入數據的數量
T(n) 表示一段代碼的總執行時間
f(n) 表示一段代碼的總執行次數
O 表示代碼的執行時間 T(n) 和 執行次數f(n) 成正比
完整的公式看着就很麻煩,別着急,這個公式只要了解一下就可以了,為的就是讓你知道我們表示算法復雜度的 O()
是怎么來的,我們平時表示算法復雜度主要就是用 O()
,讀作大歐表示法,是字母O不是零
只用一個 O()
表示,這樣看起來立馬就容易理解多了
回到剛才的兩個例子,就是上面的兩個函數
- 第一個函數執行了3次,用復雜度表示就是 O(3)
- 第二個函數執行了3n + 4次,復雜度就是 O(3n+4)
這樣有沒有覺得還是很麻煩,因為如果函數邏輯一樣的,只是執行次數差個幾次,像O(3) 和 O(4),有什么差別?還要寫成兩種就有點多此一舉了,所以復雜度里有統一的簡化的表示法,這個執行時間簡化的估算值就是我們最終的時間復雜度
簡化的過程如下
- 如果只是常數直接估算為1,O(3) 的時間復雜度就是
O(1)
,不是說只執行了1次,而是對常量級時間復雜度的一種表示法。一般情況下,只要算法里沒有循環和遞歸,就算有上萬行代碼,時間復雜度也是O(1) - O(3n+4) 里常數4對於總執行次數的幾乎沒有影響,直接忽略不計,系數 3 影響也不大,因為3n和n都是一個量級的,所以作為系數的常數3也估算為1或者可以理解為去掉系數,所以 O(3n+4) 的時間復雜度為
O(n)
- 如果是多項式,只需要保留n的最高次項,
O( 666n³ + 666n² + n )
,這個復雜度里面的最高次項是n的3次方。因為隨着n的增大,后面的項的增長遠遠不及n的最高次項大,所以低於這個次項的直接忽略不計,常數也忽略不計,簡化后的時間復雜度為 O(n³)
這里如果沒有理解的話,暫停理解一下
接下來結合栗子,看一下常見的時間復雜度
常用時間復雜度
O(1)
上面說了,一般情況下,只要算法里沒有循環和遞歸,就算有上萬行代碼,時間復雜度也是 O(1)
,因為它的執行次數不會隨着任何一個變量的增大而變長,比如下面這樣
function foo(){ let n = 1 let b = n * 100 if(b === 100){ console.log("開始吃糖") } console.log("我吃了1顆糖") console.log("我吃了2顆糖") ...... console.log("我吃了10000顆糖") }
O(n)
上面也介紹了 O(n),總的來說 只有一層循環或者遞歸等,時間復雜度就是 O(n)
,比如下面這樣
function foo1(n){ for( let i = 0; i < n; i++){ console.log("我吃了一顆糖") } } function foo2(n){ while( --n > 0){ console.log("我吃了一顆糖") } } function foo3(n){ console.log("我吃了一顆糖") --n > 0 && foo3(n) }
O(n²)
比如嵌套循環,如下面這樣的,里層循環執行 n 次,外層循環也執行 n 次,總執行次數就是 n x n,時間復雜度就是 n 的平方,也就是 O(n²)
。假設 n 是 10,那么里面的就會打印 10 x 10 = 100 次
function foo1(n){ for( let i = 0; i < n; i++){ for( let j = 0; j < n; j++){ console.log("我吃了一顆糖") } } }
還有這樣的,總執行次數為 n + n²,上面說了,如果是多項式,取最高次項,所以這個時間復雜度也是 O(n²)
function foo2(n){ for( let k = 0; k < n; k++){ console.log("我吃了一顆糖") } for( let i = 0; i < n; i++){ for( let j = 0; j < n; j++){ console.log("我吃了一顆糖") } } } //或者下面這樣,以運行時間最長的,作為時間復雜度的依據,所以下面的時間復雜度就是 O(n²) function foo3(n){ if( n > 100){ for( let k = 0; k < n; k++){ console.log("我吃了一顆糖") } }else{ for( let i = 0; i < n; i++){ for( let j = 0; j < n; j++){ console.log("我吃了一顆糖") } } } }
O(logn)
舉個栗子,這里有一包糖
這包糖里有16顆,每天吃這一包糖的一半,請問多少天吃完?
意思就是16不斷除以2,除幾次之后等於1?用代碼表示
function foo1(n){ let day = 0 while(n > 1){ n = n/2 day++ } return day } console.log( foo1(16) ) // 4
循環次數的影響主要來源於 n/2 ,這個時間復雜度就是 O(logn)
,這個復雜度是怎么來的呢,別着急,繼續看
再比如下面這樣
function foo2(n){ for(let i = 0; i < n; i *= 2){ console.log("一天") } } foo2( 16 )
里面的打印執行了 4 次,循環次數主要影響來源於 i *= 2 ,這個時間復雜度也是 O(logn)
這個 O(logn) 是怎么來的,這里補充一個小學三年級數學的知識點,對數,我們看一張圖
沒有理解的話再看一下,理解一下規律
真數
:就是真數,這道題里就是16底數
:就是值變化的規律,比如每次循環都是i*=2,這個乘以2就是規律。比如1,2,3,4,5...這樣的值的話,底就是1,每個數變化的規律是+1嘛對數
:在這道題里可以理解成x2乘了多少次,這個次數
仔細觀察規律就會發現這道題里底數是 2,而我們要求的天數就是這個對數4,在對數里有一個表達公式
a^b = n 讀作以a為底,b的對數=n,在這道題里我們知道a和n的值,也就是 2^b = 16 然后求 b
把這個公式轉換一下的寫法如下
log(a) n = b 在這道題里就是 log(2) 16 = ? 答案就是 4
公式是固定的,這個16不是固定的,是我們傳進去的 n,所以可以理解為這道題就是求 log(2)n = ?
用時間復雜度表示就是 O(log(2)n),由於時間復雜度需要去掉常數和系數,而log的底數跟系數是一樣的,所以也需要去掉,所以最后這個正確的時間復雜度就是 O(logn)
emmmmm.....
沒有理解的話,可以暫停理解一下
其他還有一些時間復雜度,我由快到慢排列了一下,如下表順序
復雜度 | 名稱 |
---|---|
O(1) |
常數復雜度 |
O(logn) |
對數復雜度 |
O(n) |
線性時間復雜度 |
O(nlogn) |
線性對數復雜度 |
O(n²) |
平方 |
O(n³) |
立方 |
O(2^ n ) |
指數,一點數據量就卡的不行 |
O(n!) |
階乘,就更慢了 |
這些時間復雜度有什么區別呢,看張圖
隨着數據量或者 n 的增大,時間復雜度也隨之增加,也就是執行時間的增加,會越來越慢,越來越卡
總的來說時間復雜度就是執行時間增長的趨勢,那么空間復雜度就是存儲空間增長的趨勢
什么是空間復雜度
空間復雜度就是算法需要多少內存,占用了多少空間
常用的空間復雜度有 O(1)
、O(n)
、O(n²)
O(1)
只要不會因為算法里的執行,導致額外的空間增長,就算是一萬行,空間復雜度也是 O(1)
,比如下面這樣,時間復雜度也是 O(1)
function foo(){ let n = 1 let b = n * 100 if(b === 100){ console.log("開始吃糖") } console.log("我吃了1顆糖") console.log("我吃了2顆糖") ...... console.log("我吃了10000顆糖") }
O(n)
比如下面這樣,n 的數值越大,算法需要分配的空間就需要越多,來存儲數組里的值,所以它的空間復雜度就是 O(n)
,時間復雜度也是 O(n)
function foo(n){ let arr = [] for( let i = 1; i < n; i++ ) { arr[i] = i } }
O(n²)
O(n²) 這種空間復雜度一般出現在比如二維數組,或是矩陣的情況下
不用說,你肯定明白是啥情況啦
就是遍歷生成類似這樣格式的
let arr = [ [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5] ]
結語
想要學好算法,就必須要理解復雜度這個重要基石
復雜度分析不難,關鍵還是在於多練。每次看到代碼的時候,簡單的一眼就能看出復雜度,難的稍微分析一下也能得出答案。推薦去 leetCode 刷題,App或者PC端都可以。