前言
在做一件事的時候,我們總是期望著可以付出最少的時間、精力或者金錢來獲得最大的回報,這個類比到算法上也同樣適用,那就是花最少的時間和最少的存儲做成最棒的解決辦法,所以好的算法應該具備時效高和存儲低的特點。這里的「時效」是指時間效率,也就是算法的執行時間,對于同一個問題的多種不同解決算法,執行時間越短的算法效率越高,越長的效率越低;「存儲」是指算法在執行的時候需要的存儲空間,主要是指算法程序運行的時候所占用的內存空間。
如何評價算法的好壞 - 大 O 表示法
大 O 表示法
首先我們先來說時間效率的這個問題,這里的時間效率就是指的算法的執行時間,時間的快慢本來就是一個相對的概念,那么到了算法上,我們該用怎樣的度量指標去度量一個算法的時間效率(執行時間)呢?
剛開始我們想出了一種事后統計方法,我稱它為「馬后炮式」,顧名思義,就是對于要解決的某個問題,費盡心思想了 n 種解法,提前寫好算法程序,然后攢了一堆數據,讓它們分別在電腦上跑,跑完了然后比較程序的運行時間,根據這個來判斷算法時效的高低。這種的判斷技術計算的是我們日常所用的時間,但這并不是一個對我們來說有用的度量指標,因為它還依賴于運行的機器、所用的編程語言、編譯器等等等等。相反,我們需要的是一個不依賴于所用機器或者編程語言的度量指標,這種度量指標可以幫助我們判斷算法的優劣,并且可以用來比較算法的具體實現。
算法的 最好情況、最壞情況和平均情況
對于「最優情況」,沒有什么大的價值,因為它沒有提供什么有用信息,反應的只是最樂觀最理想的情況,沒有參考價值。「平均情況」是對算法的一個全面評價,因為它完整全面的反映了這個算法的性質,但從另一方面來說,這種衡量并沒有什么保證,并不是每個運算都能在這種情況內完成。而對于「最壞情況」,它提供了一種保證,這個保證運行時間將不會再壞了,所以一般我們所算的時間復雜度是最壞情況下的時間復雜度,這和我們平時做事要考慮到最壞的情況是一個道理。
在我們之后的算法學習過程中,會遇到各種各樣的數量級函數,下面我給大家列舉幾種常見的數量級函數:
算法性能的評價曲線
在上圖中,我們可以看到當 n 很小時,函數之間不易區分,很難說誰處于主導地位,但是當 n 增大時,我們就能看到很明顯的區別,誰是老大一目了然:
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
求解算法的時間復雜度的具體步驟
⑴ 找出算法中的基本語句;
算法中執行次數最多的那條語句就是基本語句,通常是最內層循環的循環體。
⑵ 計算基本語句的執行次數的數量級;
只需計算基本語句執行次數的數量級,這就意味著只要保證基本語句執行次數的函數中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的系數。這樣能夠簡化算法分析,并且使注意力集中在最重要的一點上:增長率。
⑶ 用大Ο記號表示算法的時間性能。
將基本語句執行次數的數量級放入大Ο記號中。
如果算法中包含嵌套的循環,則基本語句通常是最內層的循環體,如果算法中包含并列的循環,則將并列循環的時間復雜度相加。例如:
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, 非確定多項式)問題。
(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)),則 T1T2=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是一個正常數
時間復雜度進行示例說明:
(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).
一般情況下,對步進循環語句只需考慮循環體中語句的執行次數,忽略該語句中步長加1、終值判別、控制轉移等成分,當有若干個循環語句時,算法的時間復雜度是由嵌套層數最多的循環語句中最內層語句的頻度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*).
(6)常用的算法的時間復雜度和空間復雜度
一個經驗規則:其中c是一個常量,如果一個算法的復雜度為c 、 log2n 、n 、 n*log2n ,那么這個算法時間效率比較高 ,如果是2n ,3n ,n!,那么稍微大一些的n就會令這個算法不能動了,居于中間的幾個則差強人意。
空間復雜度
*** 一個算法的空間復雜度(Space Complexity)S(n)定義為該算法所耗費的存儲空間,它也是問題規模n的函數。漸近空間復雜度也常常簡稱為空間復雜度。***
類比于時間復雜度的討論,一個算法的空間復雜度是指該算法所耗費的存儲空間,計算公式計作:S(n) = O(f(n))。其中 n 也為數據的規模,f(n) 在這里指的是 n 所占存儲空間的函數。
一般情況下,我們的程序在機器上運行時,刨去需要存儲程序本身的輸入數據等之外,還需要存儲對數據操作的「存儲單元」。如果輸入數據所占空間和算法無關,只取決于問題本身,那么只需要分析算法在實現過程中所占的「輔助單元」即可。如果所需的輔助單元是個常數,那么空間復雜度就是 O(1)。
空間復雜度其實在這里更多的是說一下這個概念,
如當一個算法的空間復雜度為一個常量,即不隨被處理數據量n的大小而改變時,可表示為O(1);當一個算法的空間復雜度與以2為底的n的對數成正比時,可表示為0(10g2n);當一個算法的空I司復雜度與n成線性比例關系時,可表示為0(n).若形參為數組,則只需要為它分配一個存儲由實參傳送來的一個地址指針的空間,即一個機器字長空間;若形參為引用方式,則也只需要為其分配存儲一個地址的空間,用它來存儲對應實參變量的地址,以便由系統自動引用實參變量。
因為當今硬件的存儲量級比較大,一般不會為了稍微減少一點兒空間復雜度而大動干戈,更多的是去想怎么優化算法的時間復雜度。
所以我們在日常寫代碼的時候就衍生出了用「空間換時間」的做法,并且成為常態。比如我們在求解斐波那契數列數列的時候我們可以直接用公式去遞歸求,用哪個求哪個,同樣也可以先把很多結果都算出來保存起來,然后用到哪個直接調用,這就是典型的用空間換時間的做法,但是你說這兩種具體哪個好,偉大的馬克思告訴我們「具體問題具體分析」。
【1】如果算法的執行時間不隨著問題規模n的增加而增長,即使算法中有上千條語句,其執行時間也不過是一個較大的常數。此類算法的時間復雜度是O(1)。
x=91; y=100;
while(y>0) if(x>100) {x=x-10;y--;} else x++;
解答: T(n)=O(1),
這個程序看起來有點嚇人,總共循環運行了1100次,但是我們看到n沒有?
沒。這段程序的運行是和n無關的,
就算它再循環一萬年,我們也不管他,只是一個常數階的函數
【2】當有若干個循環語句時,算法的時間復雜度是由嵌套層數最多的循環語句中最內層語句的頻度f(n)決定的。
x=1;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
for(k=1;k<=j;k++)
x++;
該程序段中頻度最大的語句是(5),內循環的執行次數雖然與問題規模n沒有直接關系,但是卻與外層循環的變量取值有關,而最外層循環的次數直接與n有關,因此可以從內層循環向外層分析語句(5)的執行次數: 則該程序段的時間復雜度為T(n)=O(n3/6+低次項)=O(n3)
【3】算法的時間復雜度不僅僅依賴于問題的規模,還與輸入實例的初始狀態有關。
在數值A[0..n-1]中查找給定值K的算法大致如下:
i=n-1;
while(i>=0&&(A[i]!=k))
i--;
return i;
此算法中的語句(3)的頻度不僅與問題規模n有關,還與輸入實例中A的各元素取值及K的取值有關: ①若A中沒有與K相等的元素,則語句(3)的頻度f(n)=n; ②若A的最后一個元素等于K,則語句(3)的頻度f(n)是常數0。
(5)時間復雜度評價性能
有兩個算法A1和A2求解同一問題,時間復雜度分別是T1(n)=100n2,T2(n)=5n3。(1)當輸入量n<20時,有T1(n)>T2(n),后者花費的時間較少。(2)隨著問題規模n的增大,兩個算法的時間開銷之比5n3/100n2=n/20亦隨著增大。即當問題規模較大時,算法A1比算法A2要有效地多。它們的漸近時間復雜度O(n2)和O(n3)從宏觀上評價了這兩個算法在時間方面的質量。在算法分析時,往往對算法的時間復雜度和漸近時間復雜度不予區分,而經常是將漸近時間復雜度T(n)=O(f(n))簡稱為時間復雜度,其中的f(n)一般是算法中頻度最大的語句頻度。