一般,對於一個給定的算法,咱們要作 兩項分析。第一是從數學上證實算法的正確性,這一步主要用到形式化證實的方法及相關推理模式,如循環不變式、數學概括法等。而在證實算法是正確的基礎上,第二部就是分析算法的時間複雜度。算法的時間複雜度反映了程序執行時間隨輸入規模增加而增加的量級,在很大程度上能很好反映出算法的優劣與否。所以,做爲程序員,掌握基本的算法時間複雜度分析方法是頗有必要的。
算法執行時間需經過依據該算法編制的程序在計算機上運行時所消耗的時間來度量。而度量一個程序的執行時間一般有兩種方法。html
1、過後統計的方法java
這種方法可行,但不是一個好的方法。該方法有兩個缺陷:一是要想對設計的算法的運行性能進行評測,必須先依據算法編制相應的程序並實際運行;二是所得時間的統計量依賴於計算機的硬件、軟件等環境因素,有時容易掩蓋算法自己的優點。程序員
2、事前分析估算的方法算法
因過後統計方法更多的依賴於計算機的硬件、軟件等環境因素,有時容易掩蓋算法自己的優劣。所以人們經常採用事前分析估算的方法。數組
在編寫程序前,依據統計方法對算法進行估算。一個用高級語言編寫的程序在計算機上運行時所消耗的時間取決於下列因素:函數
(1). 算法採用的策略、方法;(2). 編譯產生的代碼質量;(3). 問題的輸入規模;(4). 機器執行指令的速度。post
一個算法是由控制結構(順序、分支和循環3種)和原操做(指固有數據類型的操做)構成的,則算法時間取決於二者的綜合效果。爲了便於比較同一個問題的不一樣算法,一般的作法是,從算法中選取一種對於所研究的問題(或算法類型)來講是基本操做的原操做,以該基本操做的重複執行的次數做爲算法的時間量度。性能
一、時間複雜度
(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)) 爲算法的漸進時間複雜度,簡稱時間複雜度。測試
另外,上面公式中用到的 Landau符號實際上是由德國數論學家保羅·巴赫曼(Paul Bachmann)在其1892年的著做《解析數論》首先引入,由另外一位德國數論學家艾德蒙·朗道(Edmund Landau)推廣。Landau符號的做用在於用簡單的函數來描述複雜函數行爲,給出一個上或下(確)界。在計算算法複雜度時通常只用到大O符號,Landau符號體系中的小o符號、Θ符號等等比較不經常使用。這裏的O,最初是用大寫希臘字母,但如今都用大寫英語字母O;小o符號也是用小寫英語字母o,Θ符號則維持大寫希臘字母Θ。
T (n) = Ο(f (n)) 表示存在一個常數C,使得在當n趨於正無窮時總有 T (n) ≤ C * f(n)。簡單來講,就是T(n)在n趨於正無窮時最大也就跟f(n)差很少大。也就是說當n趨於正無窮時T (n)的上界是C * f(n)。其雖然對f(n)沒有規定,可是通常都是取儘量簡單的函數。例如,O(2n2+n +1) = O (3n2+n+3) = O (7n2 + n) = O ( n2 ) ,通常都只用O(n2)表示就能夠了。注意到大O符號裏隱藏着一個常數C,因此f(n)裏通常不加係數。若是把T(n)當作一棵樹,那麼O(f(n))所表達的就是樹幹,只關心其中的主幹,其餘的細枝末節全都拋棄無論。
在各類不一樣算法中,若算法中語句執行次數爲一個常數,則時間複雜度爲O(1),另外,在時間頻度不相同時,時間複雜度有可能相同,如T(n)=n2+3n+4與T(n)=4n2+2n+1它們的頻度不一樣,但時間複雜度相同,都爲O(n2)。 按數量級遞增排列,常見的時間複雜度有:常數階O(1),對數階O(log2n),線性階O(n), 線性對數階O(nlog2n),平方階O(n2),立方階O(n3),..., k次方階O(nk),指數階O(2n)。隨着問題規模n的不斷增大,上述時間複雜度不斷增大,算法的執行效率越低。spa
從圖中可見,咱們應該儘量選用多項式階O(nk)的算法,而不但願用指數階的算法。
常見的算法時間複雜度由小到大依次爲:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)
通常狀況下,對一個問題(或一類算法)只需選擇一種基本操做來討論算法的時間複雜度便可,有時也須要同時考慮幾種基本操做,甚至能夠對不一樣的操做賦予不一樣的權值,以反映執行不一樣操做所需的相對時間,這種作法便於綜合比較解決同一問題的兩種徹底不一樣的算法。
(3)求解算法的時間複雜度的具體步驟是:
⑴ 找出算法中的基本語句;
算法中執行次數最多的那條語句就是基本語句,一般是最內層循環的循環體。
⑵ 計算基本語句的執行次數的數量級;
只需計算基本語句執行次數的數量級,這就意味着只要保證基本語句執行次數的函數中的最高次冪正確便可,能夠忽略全部低次冪和最高次冪的係數。這樣可以簡化算法分析,而且使注意力集中在最重要的一點上:增加率。
⑶ 用大Ο記號表示算法的時間性能。
將基本語句執行次數的數量級放入大Ο記號中。
若是算法中包含嵌套的循環,則基本語句一般是最內層的循環體,若是算法中包含並列的循環,則將並列循環的時間複雜度相加。例如:
- for (i=1; i<=n; i++)
- x++;
- for (i=1; i<=n; i++)
- for (j=1; j<=n; j++)
- x++;
第一個for循環的時間複雜度爲Ο(n),第二個for循環的時間複雜度爲Ο(n2),則整個算法的時間複雜度爲Ο(n+n2)=Ο(n2)。
Ο(1)表示基本語句的執行次數是一個常數,通常來講,只要算法中不存在循環語句,其時間複雜度就是Ο(1)。其中Ο(log2n)、Ο(n)、 Ο(nlog2n)、Ο(n2)和Ο(n3)稱爲多項式時間,而Ο(2n)和Ο(n!)稱爲指數時間。計算機科學家廣泛認爲前者(即多項式時間複雜度的算法)是有效算法,把這類問題稱爲P(Polynomial,多項式)類問題,而把後者(即指數時間複雜度的算法)稱爲NP(Non-Deterministic Polynomial, 非肯定多項式)問題。
通常來講多項式級的複雜度是能夠接受的,不少問題都有多項式級的解——也就是說,這樣的問題,對於一個規模是n的輸入,在n^k的時間內獲得結果,稱爲P問題。有些問題要複雜些,沒有多項式時間的解,可是能夠在多項式時間裏驗證某個猜想是否是正確。好比問4294967297是否是質數?若是要直接入手的話,那麼要把小於4294967297的平方根的全部素數都拿出來,看看能不能整除。還好歐拉告訴咱們,這個數等於641和6700417的乘積,不是素數,很好驗證的,順便麻煩轉告費馬他的猜測不成立。大數分解、Hamilton迴路之類的問題,都是能夠多項式時間內驗證一個「解」是否正確,這類問題叫作NP問題。
(4)在計算算法時間複雜度時有如下幾個簡單的程序分析法則:
(1).對於一些簡單的輸入輸出語句或賦值語句,近似認爲須要O(1)時間
(2).對於順序結構,須要依次執行一系列語句所用的時間可採用大O下"求和法則"
求和法則:是指若算法的2個部分時間複雜度分別爲 T1(n)=O(f(n))和 T2(n)=O(g(n)),則 T1(n)+T2(n)=O(max(f(n), g(n)))
特別地,若T1(m)=O(f(m)), T2(n)=O(g(n)),則 T1(m)+T2(n)=O(f(m) + g(n))
(3).對於選擇結構,如if語句,它的主要時間耗費是在執行then字句或else字句所用的時間,需注意的是檢驗條件也須要O(1)時間
(4).對於循環結構,循環語句的運行時間主要體如今屢次迭代中執行循環體以及檢驗循環條件的時間耗費,通常可用大O下"乘法法則"
乘法法則: 是指若算法的2個部分時間複雜度分別爲 T1(n)=O(f(n))和 T2(n)=O(g(n)),則 T1*T2=O(f(n)*g(n))
(5).對於複雜的算法,能夠將它分紅幾個容易估算的部分,而後利用求和法則和乘法法則技術整個算法的時間複雜度
另外還有如下2個運算法則:(1) 若g(n)=O(f(n)),則O(f(n))+ O(g(n))= O(f(n));(2) O(Cf(n)) = O(f(n)),其中C是一個正常數
(5)下面分別對幾個常見的時間複雜度進行示例說明:
(1)、O(1)
Temp=i; i=j; j=temp;
以上三條單個語句的頻度均爲1,該程序段的執行時間是一個與問題規模n無關的常數。算法的時間複雜度爲常數階,記做T(n)=O(1)。注意:若是算法的執行時間不隨着問題規模n的增長而增加,即便算法中有上千條語句,其執行時間也不過是一個較大的常數。此類算法的時間複雜度是O(1)。
(2)、O(n2)
2.1. 交換i和j的內容
- sum=0; (一次)
- for(i=1;i<=n;i++) (n+1次)
- for(j=1;j<=n;j++) (n2次)
- sum++; (n2次)
解:由於Θ(2n2+n+1)=n2(Θ即:去低階項,去掉常數項,去掉高階項的常參獲得),因此T(n)= =O(n2);
2.2.
- for (i=1;i<n;i++)
- {
- y=y+1; ①
- for (j=0;j<=(2*n);j++)
- x++; ②
- }
解: 語句1的頻度是n-1
語句2的頻度是(n-1)*(2n+1)=2n2-n-1
f(n)=2n2-n-1+(n-1)=2n2-2;
又Θ(2n2-2)=n2
該程序的時間複雜度T(n)=O(n2).
通常狀況下,對步進循環語句只需考慮循環體中語句的執行次數,忽略該語句中步長加一、終值判別、控制轉移等成分,當有若干個循環語句時,算法的時間複雜度是由嵌套層數最多的循環語句中最內層語句的頻度f(n)決定的。
(3)、O(n)
- a=0;
- b=1; ①
- for (i=1;i<=n;i++) ②
- {
- s=a+b; ③
- b=a; ④
- a=s; ⑤
- }
解: 語句1的頻度:2,
語句2的頻度: n,
語句3的頻度: n-1,
語句4的頻度:n-1,
語句5的頻度:n-1,
T(n)=2+n+3(n-1)=4n-1=O(n).
(4)、O(log2n)
- i=1; ①
- hile (i<=n)
- i=i*2; ②
解: 語句1的頻度是1,
設語句2的頻度是f(n), 則:2^f(n)<=n;f(n)<=log2n
取最大值f(n)=log2n,
T(n)=O(log2n )
(5)、O(n3)
- for(i=0;i<n;i++)
- {
- for(j=0;j<i;j++)
- {
- for(k=0;k<j;k++)
- x=x+2;
- }
- }
解:當i=m, j=k的時候,內層循環的次數爲k當i=m時, j 能夠取 0,1,...,m-1 , 因此這裏最內循環共進行了0+1+...+m-1=(m-1)m/2次因此,i從0取到n, 則循環共進行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6因此時間複雜度爲O(n3).
(5)經常使用的算法的時間複雜度和空間複雜度
一個經驗規則:其中c是一個常量,若是一個算法的複雜度爲c 、 log2n 、n 、 n*log2n ,那麼這個算法時間效率比較高 ,若是是2n ,3n ,n!,那麼稍微大一些的n就會令這個算法不能動了,居於中間的幾個則差強人意。
算法時間複雜度分析是一個很重要的問題,任何一個程序員都應該熟練掌握其概念和基本方法,並且要善於從數學層面上探尋其本質,才能準確理解其內涵。
二、算法的空間複雜度
相似於時間複雜度的討論,一個算法的空間複雜度(Space Complexity)S(n)定義爲該算法所耗費的存儲空間,它也是問題規模n的函數。漸近空間複雜度也經常簡稱爲空間複雜度。
空間複雜度(Space Complexity)是對一個算法在運行過程當中臨時佔用存儲空間大小的量度。一個算法在計算機存儲器上所佔用的存儲空間,包括存儲算法自己所佔用的存儲空間,算法的輸入輸出數據所佔用的存儲空間和算法在運行過程當中臨時佔用的存儲空間這三個方面。算法的輸入輸出數據所佔用的存儲空間是由要解決的問題決定的,是經過參數表由調用函數傳遞而來的,它不隨本算法的不一樣而改變。存儲算法自己所佔用的存儲空間與算法書寫的長短成正比,要壓縮這方面的存儲空間,就必須編寫出較短的算法。算法在運行過程當中臨時佔用的存儲空間隨算法的不一樣而異,有的算法只須要佔用少許的臨時工做單元,並且不隨問題規模的大小而改變,咱們稱這種算法是「就地\"進行的,是節省存儲的算法,如這一節介紹過的幾個算法都是如此;有的算法須要佔用的臨時工做單元數與解決問題的規模n有關,它隨着n的增大而增大,當n較大時,將佔用較多的存儲單元,例如將在第九章介紹的快速排序和歸併排序算法就屬於這種狀況。
如當一個算法的空間複雜度爲一個常量,即不隨被處理數據量n的大小而改變時,可表示爲O(1);當一個算法的空間複雜度與以2爲底的n的對數成正比時,可表示爲0(10g2n);當一個算法的空I司複雜度與n成線性比例關係時,可表示爲0(n).若形參爲數組,則只須要爲它分配一個存儲由實參傳送來的一個地址指針的空間,即一個機器字長空間;若形參爲引用方式,則也只須要爲其分配存儲一個地址的空間,用它來存儲對應實參變量的地址,以便由系統自動引用實參變量。
while(y>0) if(x>100) {x=x-10;y--;} else x++;
解答: T(n)=O(1),
這個程序看起來有點嚇人,總共循環運行了1100次,可是咱們看到n沒有?
沒。這段程序的運行是和n無關的,
就算它再循環一萬年,咱們也無論他,只是一個常數階的函數