爲什麼需要複雜度分析
對於一段算法,如何判定它是否高效?如何針對現有的數據量級對於這段代碼以及數據增長之後的空間、時間進行預估?可能有人會認爲,這個問題很簡單,直接寫一段測試代碼,實際運行一下就知道了。這種統計的方法叫做:事後統計法
單純的事後統計法存在幾點問題:
- 統計結果完全依賴於你的硬件環境。例如你用i8的內核肯定比你i3內核的機器運行的快。
- 測試結果收數據量級規模以及規則影響很大。例如排序算法,如果已經有序,消耗時間可以忽略不計。
- 當需要很大的數據量級去測試的時候,可能及其消耗時間與經歷。並且測出來的可能壓根不準確。而有很多時候我們只是需要去預估的時候也許壓根也沒有這麼大的數據去測試。
因此我們就需要有一個能單純統計這段算法效率、空間複雜度的方法,也就是這裏的算法複雜度。
什麼是算法複雜度
**注意:**算法複雜度並不是代表一段代碼實際執行的時間與實際消耗的空間,只是表示代碼執行時間/空間隨數據規模增長的變化趨勢。因此全稱爲漸進時間/空間複雜度。
算法複雜度又分爲兩塊:
- 時間複雜度:表示代碼執行時間隨數據規模增長的變化趨勢。
- 空間複雜度:表示代碼執行消耗空間隨數據規模增長的變化趨勢。
時間複雜度分析
1.如何計算?
首先,我們假設每行代碼執行時間爲固定時間unit_time。我們認爲這值是一個常量值,不會隨着硬件條件以及數據規模的改變而改變。
func test(n int) int {
a := 0 //1
for i:= 0; i<n; i++ { //2,3
a = a+i //4
}
return a
}
上面是一個最簡單的循環,那麼按照我們上面所說,實際執行時間就是:unit_time + unit_time + 2n*unit_time,for循環的第一個初始化語句(2)可以認爲就執行了一次,因此1、2的時間都是unit_time,之後的判斷邏輯(3)與循環裏面的邏輯(4),都執行了n次,因此時間爲2n*unit_time,那麼使用大O表示法就是:T(n) = O(2n+2),當n無限大的時候,我們就可以忽略其餘的常數,因此這段算法的的時間複雜度爲O(n)。
2.計算時間複雜度的一些技巧
-
忽略常數
- 以上面的例子爲例,單位時間的常量,我們均在認爲n無限大的情況下,捨棄掉。
-
加法—取最大
func test(n int) int { a := 0 for i:= 0; i<100; i++ { a = a+i } b := 0 for i:= 0; i<n; i++ { b = b+i } c := 0 for i:= 0; i<n; i++ { for j:=0; j<n; j++ { c = c+j } } return a + b + c }
這裏有三個循環,最終返回三個數的和,那麼就是100*unit_time+n*unit_time+n*unit_time,首先第一個循環循環次數是一個固定值,按照我們上面所說,不管是100、1000、10000均不是一個隨着n變化的常量,因此直接捨棄,第二個循環複雜度均爲O(n),第三個循環複雜度爲O(n*n)因此最終的複雜度爲***O(n2)**n的平方。
-
乘法—相乘
上面例子中的第三個循環就是標準的乘法運算,複雜度出現嵌套的情況下,直接相乘即可。
常見時間複雜度量級分析
-
多項式量級
-
常量階 O(1)
- 一般來說,只要代碼中沒有循環、遞歸等,就算其中有無數行代碼,時間複雜度也爲O(1)
-
對數階 O(logn)
i := 1 while ( i<= n ) { i = i * 2 }
這樣的一個算法複雜度爲O(log2n)。
如果我們將i*2改成i*3,那麼複雜度爲O(log3n)。
忽略係數,時間複雜度則爲O(logn),那麼**O(nlogn)**則是logn的算法循環了n次。
-
線性階 O(n)
- 上一節如何計算與計算其中已經介紹了O(n)
-
線性對數階 O(nlogn)
- **O(nlogn)**則是logn的算法循環了n次
-
平方/立方/K次方階 O(n2/n3/nk)
- 上一節如何計算與計算其中已經介紹了平方/立方/K次方階
-
-
非多項式量級
- 指數階 O(2n)
- 階乘階 O(n!)
空間複雜度
空間複雜度和時間複雜度一致,包括計算方法、技巧均與時間複雜度一致。
常用複雜度趨勢圖
歡迎大家關注我的個人博客:http://blog.geek-scorpion.com/