前言
算法復雜度是指算法在編寫成可執行程序后,運行時所需要的資源,資源包括時間資源和內存資源。
復雜度也叫漸進復雜度,包括時間復雜度和空間復雜度,用來粗略分析執行效率與數據規模之間的增長趨勢關系,越高階復雜度的算法,執行效率越低。
復雜度分析是數據結構與算法的核心精髓,指在不依賴硬件、宿主環境、數據集的情況下,粗略推導,考究出算法的效率和資源消耗情況。
時間&空間復雜度
數據結構和算法本身解決的是“快”和“省”的問題,即如何讓代碼運行的更快,如何讓代碼更節省存儲空間。所以執行效率是算法非常重要的考量標准。
而衡量一個算法的執行效率就需要:時間復雜度分析和空間復雜度分析。
事后統計法:是讓代碼執行一遍,通過統計、監控等,得到算法執行時間和占用內存大小,事后統計法評估執行效率的方式並沒有問題。但它存在2個局限性:
1.評估受測試環境所影響
2.評估受數據規模所影響
所以需要一個不依賴具體的運行環境及測試數據就可以粗略地估計執行效率的方法。這就需要:時間復雜度分析和空間復雜度分析。
如何進行復雜度分析
下面有段代碼非常的簡單,求1,2,3...n的累加和。
如何在不運行代碼的情況下,粗略的推導代碼的執行時間呢?以Demo1為例:
1 int Demo1(int n) 2 { 3 int sum = 0; //1個unit_time 4 for (int i = 1; i <= n; ++i)//n個unit_time 5 { 6 sum = sum + i; //n個unit_time 7 } 8 return sum; 9 } 10 /* 11 * 作者:Jonins 12 * 出處:http://www.cnblogs.com/jonins/ 13 */
首先在CPU的角度看待程序,那么每行代碼執行的操作都是類似的:
1.讀數據
2.寫數據
3.運算
盡管每段代碼執行的時間都不一定一樣,但是我們這里只是粗略的估計,所以可以假設每行代碼執行的時間都一樣,為:單位時間(unit_time)。即每一行代碼的執行時間為:1個單位時間(unit_time)。
以此假設基礎,我們來分析上段代碼的執行時間:
第3行:執行了1次,所以需要1個單位時間的執行時間。
第4行:因為執行了n遍,所以需要n個單位時間的執行時間。
第6行:因為執行了n遍,所以需要n個單位時間的執行時間。
所以這段代碼的總執行時間就是:
(2n+1)*unit_time
本質:若要獨立於機器的軟、硬件系統來分析算法的時間耗費,則假設每條語句執行一次所需的時間均是單位時間,一個算法的時間耗費就是該算法中所有語句的執行時間之和。
按照這個分析方式,再分析下面Demo2代碼:
1 public int Demo2(int n) 2 { 3 int sum = 0; //1個unit_time 4 for (int i = 1; i <= n; ++i) //n個unit_time 5 { 6 for (int j = 1; j <= n; ++j)//n*n 個unit_time 7 { 8 sum = sum + i* j; //n*n 個unit_time 9 } 10 } 11 return sum; 12 } 13 /* 14 * 作者:Jonins 15 * 出處:http://www.cnblogs.com/jonins/ 16 */
第3行:需要1個單位時間的執行時間。
第4行:執行了n遍,需要n個單位時間的執行時間。
第6,8行:循環執行了n2遍,所以需要2n2個單位時間的執行時間。
所以這段代碼的總執行時間是:
(2n2+n+1)*unit_time
時間復雜度&大O表示法
明白了時間復雜度分析的方法。再了解幾個概念。
1.時間頻度
一個算法執行所耗費的時間,從理論上是不能算出來的,必須上機運行測試才能知道。但我們不可能也沒有必要對每個算法都上機測試,只需知道哪個算法花費的時間多,哪個算法花費的時間少就可以了。
並且一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
2.時間復雜度
在剛才提到的時間頻度中,n通稱為問題的規模,當n不斷變化時,時間頻度T(n)也會不斷變化。但有時我們想知道它變化時呈現什么規律。為此,我們引入時間復雜度概念。
在計算機科學中,算法的時間復雜度是一個函數,它定性描述了該算法的運行時間。
一般情況下,算法中基本操作重復執行的次數是問題規模n的某個函數,用T(n)表示。
若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函數。記作T(n)=O(f(n)),稱O(f(n))為算法的漸進時間復雜度,簡稱時間復雜度。
3.大O時間復雜度表示法
當問題規模增長時, 基本操作次數必定也會增長, 而我們關心的是這個執行次數以什么樣的數量級增長。
所謂數量級可以理解為增長率。這個所謂的數量級本質上就是算法的漸近時間復雜度(asymptotic time complexity), 簡稱為時間復雜度。
由於基本操作的執行次數是問題規模n的一個函數T(n), 我們要確定這個函數T(n), 然后分析它的數量級。把擁有相同數量級的函數 f(n) 的集合表示為 O(f(n))。
O是數量級的標記(數學公式),函數f(n)為算法的增率函數(growth-rate function)。
在上面兩個分析示例中,我們雖然不知道單位時間(unit_time)的具體值,但是依然可以推導一個非常重要的規律:所有代碼的執行時間T(n)與代碼執行次數成正比。
所以我們根據這個規律總結出一個公式:
T(n)=O(f(n))
大白話解讀公式:
T(n):表示代碼的總執行時間。
n:表示數據規模大小。
f(n):表示代碼執行的次數總和,因為是一個公式,所以用f(n)表示。
O:表示代碼的執行時間T(n)與f(n)表達式成正比。
所以上面的兩個示例,用大O時間復雜度可以這樣推導表示:
Demo1時間復雜度:
T(n)=O(f(n))
因為:f(n)=(2n+1)
所以Demo1時間復雜度:T(n)= O(2n+1)
Demo2時間復雜度:
T(n)=O(f(n))
因為:f(n)=(2n2+n+1)
所以Demo1時間復雜度:T(n)= O(2n2+n+1)
大O時間復雜度,實際上並不具體表示代碼真正的執行時間,而是用來表示代碼執行時間隨數據規模增長的變化趨勢。
復雜度分析原則
1.量級最大法則:忽略常量、低階和系數
大O復雜度表示方法只表示一種變化趨勢。所以忽略掉公式中的常量、低階、系數、只關注最大階的量級就可以。
我們在分析一個算法或一段代碼的復雜度的時候,也只關注受問題規模n影響越大即相關執行次數最多的那段代碼即可。
以Demo1為例:
1 int Demo1(int n) 2 { 3 int sum = 0; //1個unit_time 4 for (int i = 1; i <= n; ++i)//n個unit_time 5 { 6 sum = sum + i; //n個unit_time 7 } 8 return sum; 9 } 10 /* 11 * 作者:Jonins 12 * 出處:http://www.cnblogs.com/jonins/ 13 */
第3行代碼是常量級的執行時間,與問題規模n無關,所以對復雜度並沒有影響。
循環次數最多的是第4、6行,所致重點關注這段代碼。這兩行代碼各被執行了n次,所以總的時間復雜度就是O(2n)。
說明:
固定的代碼循環,哪怕循環一千萬次,只要是一個已知的數,跟問題規模無關,它就是常量級的執行時間。當n無限大的時候常量可以忽略不計。
盡管這部分代碼對執行時間會有很大影響,但是在我們探討的時間復雜度概念上,時間復雜度表示的是一個算法執行效率與數據規模之前的一種變化趨勢。
所以不管常量的執行時間多大,我們都可以忽略掉它,因為它本身對增長趨勢並沒有影響。
2.加法法則:總復雜度等於同級代碼復雜度的總和
同樣的分析思路分析下面Demo3:
1 int Demo3(int n) 2 { 3 int sum_1 = 0; 4 for (int i = 1; i <= 100; i++) 5 { 6 sum_1 = sum_1 + i; 7 } 8 9 int sum_2 = 0; 10 for (int i = 1; i <= n; i++) 11 { 12 sum_2 = sum_2 + i; 13 } 14 15 int sum_3 = 0; 16 for (int j = 1; j <= n; j++) 17 { 18 for (int q = 1; q <= n; q++) 19 { 20 sum_3 = sum_3 + j * q; 21 } 22 } 23 return sum_1 + sum_2 + sum_3; 24 }
這段代碼分為3部分,分別求sum_1,sum_2,sum_3,這3段代碼是“同級的”的,所以我們可以分別分析這三段代碼的復雜度,最后相加在一起,取得總的時間復雜度。
第1段代碼:執行100次,是一個常量執行時間跟n的規模無關。
第2段代碼:執行了2n次,所以時間復雜度為O(2n)。
第3段代碼:執行了2n2+n,所以時間復雜度為O(2n2+n)。
綜合這3段代碼的時間復雜度,我們省略常量並取其中最大量級。所以整段代碼的時間復雜度為O(2n2)。
說明:加法法則公式
如果:T1(n)=O(g(n))、T2(n)=O(h(n))。
存在:T(n)=T1(n)+T2(n)。O(f(n))=O(g(n))+O(h(n))。
所以:T(n)=O(max(g(n)+h(n)))。
3.乘法法則:嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積
分析下面的Demo4,我改寫了上面Demo2為了便於理解:
1 public int Demo4(int n) 2 { 3 int sum = 0; 4 for (int i = 1; i <= n; ++i) //n個unit_time 5 { 6 sum = sum + Function(n); //n個unit_time 7 } 8 return sum; 9 } 10 public int Function(int n) 11 { 12 int sum = 0; 13 for (int j = 1; j <= n; ++j)//n 個unit_time 14 { 15 sum = sum + 1; //n 個unit_time 16 } 17 return sum; 18 } 19 /* 20 * 作者:Jonins 21 * 出處:http://www.cnblogs.com/jonins/
獨立的看嵌套外的代碼,將函數Function看作一個簡單的操作。那么第4、6行執行了n次,所以這段段代碼為2n次。
同時獨立看嵌套內的代碼,第13、15行執行了n次,所以也是2n次。但因為在嵌套內執行了n次個2n次,即n*2n。
所以我們忽略掉常量及取最高階量值,得到整體的復雜度為:T(n)=O(2n2)。
說明:
1.嵌套循環並非一定是相乘
1 public int Function1(int n) 2 { 3 int sum = 0; 4 for (int i = 1; i <= n; ++i) 5 { 6 for (int j = 1; j <= i; ++j) 7 { 8 sum = sum + 1; 9 } 10 } 11 return sum; 12 } 13 public int Function2(int n) 14 { 15 int sum = 0; 16 for (int i = 1; i <= n; ++i) 17 { 18 for (int j = 1; j <= n; ++j) 19 { 20 sum = sum + 1; 21 } 22 } 23 return sum; 24 } 25 /* 26 * 作者:Jonins 27 * 出處:http://www.cnblogs.com/jonins/ 28 */
注意觀察嵌套內循環的邊界,Function1和Function2的執行次數有極大差異。
執行一下可以看到Function1的內部循環次數為n(n+1)/2。Function2的內部循環次數才是n2。
2.乘法法則公式
如果:T1(n)=O(g(n))、T2(n)=O(h(n))。
存在:T(n)=T1(n)*T2(n)。O(f(n))=O(g(n))*O(h(n))。
所以:T(n)=O(g(n)*h(n))。
常見復雜度量級
雖然算法各有千秋代碼又千差萬別。但最基礎的復雜度並不多。根據復雜度的量級,我們可以將算法的復雜度分為兩類:多項式量級、非多項式量級。
1.多項式量級
常量階:O(1)
對數階:O(log(n))
線性階:O(n)
線性對數階:O(nlog(n))
平方階:O(n2)
立次方階:O(n3)
K次方階:O(nk)
2.非多項式量級:
指數階:O(2n)
階乘階:O(n!)
3.一些重要的說明
1.O(1)
O(1)只是常量階復雜度表示方法,並不一定指執行了一次,即使執行多次只要是一個確切的值,且不受問題規模n的影響時,我們就是用復雜度O(1)表示。
2.O(log(n))&O(nlog(n))
對數階復雜度非常常見,二分查找的時間復雜度為O(log(n)),歸並排序時間復雜度為O(nlog(n))。
通過案例了解下對數階
1 public void Function(int n) 2 { 3 int i = 1; 4 while (i < n) 5 { 6 i = i * 2; 7 } 8 }
我們着重分析第6行代碼,變量i從1開始取,每循環一次乘以2,當大於或等於n時循環解說。可以推算出變量i的值在整個循環過程中是一個等比數列,既:
20,22,23,24,25,26....=n。
而第6行的循環次數,就是數學題,存在2x=n,求解x的近似值。我們大致可以得出x=log(2n)。所以這段代碼的復雜度就是O(log(n))。
如果理解了對數階O(log(n)),那么線性對數階O(nlog(n))就很好理解,上面的乘法法則,將O(log(n))執行n遍就得到了O(nlog(n))。
3.所有對數階均為O(log2n)
上面已經講過對數階,但實際上,不管是以2為底,還是3為底,還是以x為低(x為已知量),我們都可以把所有的對數階的時間復雜度記為O(log(n))。
我們可以通過換底公式推導:
換底公式:
$ \log(a^b)=\frac{\log(c^b)}{\log(c^a)} $
推導過程:
$\because \log(2^n)=\frac{\log(x^n)}{\log(x^2)}$
$\therefore \log(x^n)=\log(2^n)*\log(x^2)$
我們已知x的值,所以log(x2)為常量,基於分析原則可以忽略系數。所以所有的對數階,我們忽略對數的“底”,統一用O(log(n))表示。
空間復雜度
學會大O表示法和時間復雜度分析,那么空間復雜度分析就非常簡單。
空間復雜度全稱就是漸進空間復雜度(asymptotic space complexity),表示算法在運行過程中臨時占用的存儲空間大小的量度。
分析原則:時間復雜度關注代碼的執行次數,而空間復雜度主要關注申請空間的數量。
常見的空間復雜度就是:O(1)、O(n)、O(n2),像對數階等量級的空間復雜度平常用不上。
空間復雜度分析相對於時間復雜度分析要簡單很多,所以在這里不再過多描述。