學習數據結構和算法的第一步面試
分析時間複雜度的時候是不考慮前邊的係數的,好比說O(1)的話,並不表明它的複雜度是1,也能夠是二、三、4...,只要是常數次的,都用O(1)表示算法
最經常使用的方式就是直接看這段代碼,它根據n的不一樣狀況會運行多少次數組
O(1) $n=100000; echo 'hello'; O(?) $n=100000; echo 'hello1'; echo 'hello2'; echo 'hello3';
第一段代碼,無論n是多少,都只執行一次,因此它的時間複雜度就是O(1)。第二個其實也同理,咱們不關心繫數是多少。雖然第二段代碼會執行3次echo輸出,可是無論n是多少,它都只執行3次,所以它的時間複雜度也是常數複雜度,也就是O(1)緩存
在看下邊兩段代碼:數據結構
O(n) for($i = 1; $i <= $n; $i++) { echo 'hello'; } O(n^2) for($i = 1; $i <= $n; $i++) { for($j = 1; $j <= $n; $j++) { echo 'hello'; } }
這兩段代碼都是隨着n的不一樣,它執行的次數也在發生變化,第一段代碼執行的次數和n是線性關係的,因此它的時間複雜度是O(n)。數據結構和算法
第二段代碼是一個嵌套循環,當n爲100的狀況下,裏邊的輸出語句就會執行10000次,所以它的時間複雜度就是O(n^2)。若是第二段代碼中的循環不是嵌套的,而是並列的,那麼它的時間複雜度應該是O(2n),由於前邊的常數係數咱們不關注,所以它的時間複雜度就是O(n)函數
O(log n) for($i = 1; $i <= $n; $i = $i*2) { echo 'hello'; } O(k^2) fib($n) { if ($n < 2) { return $n; } return fib($n-1) + fib($n-2); }
第一段代碼,當n=4時,循環執行2次,因此循環內部執行的次數和n的關係爲log2(n),所以時間複雜度爲對數複雜度O(logn)。第二段是一個Fibonacci(斐波拉契)數列,這裏是用了遞歸的這種形式,這就牽扯到了遞歸程序在執行的時候,如何計算它的時間複雜度,它的答案是k^n,k是一個常數,是一個指數級的,因此簡單的經過遞歸的方式求Fibonacci數列是很是慢的,它是指數級的時間複雜度。具體指數級的時間複雜度是怎麼獲得的,後邊會詳細說明。下邊看一下各類時間複雜度的曲線學習
從這張圖中能夠看到,當n比較小的時候,在10之內的話,不一樣的時間複雜度其實都差很少。可是若是當n繼續擴大,指數級的增加是很是快的。所以,當咱們在寫程序的時候,若是能優化時間複雜度,好比說從2^n降到n^2的話,從這個曲線來看,當n較大的時候,獲得的收益是很是高的。所以這也告訴咱們,在平時開發業務代碼的時候,必定要對本身的時間和空間複雜度有所瞭解,並且是養成習慣,寫完代碼以後,下意識的分析出這段程序的時間和空間複雜度。優化
從圖中能夠看到,若是你的時間複雜度寫砸了的話,其實帶給公司的機器或者說資源的損耗,隨着n的增大的話,是成百上千的增長,而若是你能簡化的話,對公司來講是節約不少成本的spa
對於不一樣的程序,在寫法當中完成一樣的目標,它可能會致使時間複雜度的不同。下邊看一個簡單的例題
從1加到2一直加到n,求它的和
小學學數學的時候,你們都知道了有兩種方法,方法一的話用程序暴力求解的話,就是從1循環到n累加。這個是一層循環,n爲多少,就執行多少次累加,因此它的時間複雜度就是O(n)
$sum = 0; for ($i=1; $i <=$n; $i++) { $sum += $i; }
方法二就是使用一個數學的求和公式:
y = n*(n+1)/2
用這個公式,發現程序就只有一行了,因此它的時間複雜度就是O(1)了。因此能夠看到,程序的不一樣方法,最後獲得的結果雖然是同樣的,可是它的時間複雜度很不同
遞歸的話,關鍵就是要了解它的遞歸過程,總共執行了遞歸語句多少次。若是是循環的話,很好理解,n次的循環就執行了n次。那麼遞歸的話,其實它是層層嵌套,其實不少時候,咱們就是把遞歸它的執行順序,畫出一個樹形結構,稱之爲它的遞歸狀態的遞歸樹。之前邊的求Fibonacci(斐波拉契)數列的第n項爲例
Fib:0,1,1,2,3,5,8,13,21... F(n) = F(n-1)+F(n-2)
我以前面試的時候遇到過這麼一道題,寫的是最最簡單的用遞歸的方式實現
fib($n) { if($n < 2) { retuen $n; } return fib($n-1)+fib($n-2); }
前邊有說到它的時間複雜度是O(k^n),那麼怎麼獲得的,能夠分析一下,假設此時n取6,要計算Fib(6),就看這段代碼如何執行
因此,若是想計算F(6)就引出兩個分支,F(5)和F(4),至少多出了兩次運算
若是要計算F(5)可同理獲得,須要結算F(4)和F(3)。若是要計算F(4)可同理獲得,須要結算F(3)和F(2)。這裏就能夠看到兩個現象:
正是由於有這麼多大量冗餘的計算,致使求6個數的Fibonacci數的話,就變成了2^6次方這麼一個時間複雜度。所以在面試中遇到這類題,儘可能別用這種方式寫,不然怕是直接涼涼了。能夠加一個緩存,把這些中間結果可以緩存下來(用數組或哈希存起來,有重複計算的數值,再從裏邊找),或者直接用一個循環來寫
介紹一個叫主定理的東西,這個定理爲何重要,就是由於這個主定理的話,其實它是用來解決全部遞歸的函數,怎麼來計算它的時間複雜度。主定理自己的話,數學上來證實相對比較複雜(關於主定理能夠參考維基百科:https://zh.wikipedia.org/wiki...)
也就是說,任何一個分治或者遞歸的函數,均可以算出它的時間複雜度,怎麼算就是經過這個主定理。自己比較複雜的話,那怎樣化簡爲實際可用的辦法,其實關鍵就是這四種,通常記住就能夠了
通常在各類遞歸的情形的話,有上邊這四種情形,是在面試和平時工做中會用上
二分查找(Binary search):通常發生在一個數列自己有序的時候,要在有序的數列中找到目標數,因此它每次都一分爲二,只查一邊,這樣的話,最後它的時間複雜度是O(logn)
二叉樹遍歷(Binary tree traversal):若是是二叉樹遍歷的話,它的時間複雜度爲O(n)。由於經過主定理能夠知道,它每次要一分爲二,可是每次一分爲二以後,每一邊它是相同的時間複雜度。最後它的遞推公式就變成了圖中T(n)=2T(n/2)+O(1)這樣。最後根據這個主定理就能夠推出它的運行時間爲O(n)。固然這裏也有一個簡化的思考方式,就是二叉樹的遍歷的話,會每個節點都訪問一次,且僅訪問一次,因此它的時間複雜度就是O(n)
二維矩陣(Optimal sorted matrix search):在一個排好序的二維矩陣中進行二分查找,這個時候也是經過主定理能夠得出時間複雜度是O(n),記住就能夠了
歸併排序(merge sort):全部排序最優的辦法就是nlogn,歸併排序的時間複雜度就是O(nlogn)
二叉樹的遍歷-前序、中序、後序:時間複雜度是多少?
答案是:O(n),這裏的n表明二叉樹裏邊樹的節點的總數,不論是哪一種方式遍歷,每一個節點都有且僅訪問一次,因此它的複雜度是線性於二叉樹的節點總數,也就是O(n)
圖的遍歷:時間複雜度是多少?
答案:O(n),圖中的每個節點也是有且僅訪問一次,所以時間複雜度也是O(n),n爲圖中的節點總數
搜索算法:DFS(深度優先)、BFS(廣度優先)時間複雜度是多少?
答案:O(n),後邊的文章會詳細介紹這兩種算法(n爲搜索空間中的節點總數)
二分查找:時間複雜度是多少?
答案:O(logn)
空間複雜度和時間複雜度的狀況其實相似,可是它更加的簡單。用最簡單的方式來分析便可。主要有兩個原則:
若是你的代碼中開了數組,那麼數組的長度基本上就是你的空間複雜度。好比你開了一個一維的數組,那麼你的空間複雜度就是O(n),若是開了一個二維的數組,數組長度是n^2,那麼空間複雜度基本上就是n^2
若是是有遞歸的話,那麼它遞歸最深的深度,就是你空間複雜度的最大值。若是你的程序裏邊遞歸中又開了數組,那麼空間複雜度就是二者的最大值
在快速變化的技術中尋找不變,纔是一個技術人的核心競爭力。知行合一,理論結合實踐