本章內容可分為:
如何來度量乙個演算法的執行時間呢?
演算法的時間複雜度
演算法的空間複雜度
上章中,我們提到設計演算法要盡量的提高效率,這裡效率高一般指的是演算法的執行時間。
所謂「是騾子是馬拉出來遛遛」,比較容易想到的方法就是我們把演算法跑若干次,然後拿個「計時器」在旁邊計時。這種方法便為:事後統計方法。
事後統計方法:這種方法主要是通過設計好的測試程式和資料,利用計算機計時器對不同演算法編制的程式的執行時間進行比較,從而確定演算法效率的高低。但是這種方法有很大的缺陷:
必須依據演算法事先編制好的測試程式,通常需要花費大量時間和精力。
不用測試環境差別不是一般的大
為了對演算法的評判更為科學和便捷,出現了「事前分析估算」的方法。
事前分析估算方法:在電腦程式編寫前,依據統計方法對演算法進行估算。
經總結,我們發現乙個高階語言編寫的程式在計算機上執行時所消耗的時間取決於下列因素:
演算法採用的策略和方案
編譯產生的**質量
問題的輸入規模
機器執行指令的速度
由此可見,拋開這些與計算機及硬體、軟體有關的因素,乙個程式的執行時間依賴於演算法的好壞和問題的輸入規模。(所謂的問題輸入規模是指輸入量的多少)
例如:上章中的1+2+3+…+100的求和問題。
//第一種演算法:
int i, sum =
0, n =
100;
//執行1次
for(i=
1; i <= n; i++
)
//第二種演算法:
int sun =
0, n =
100;
//執行1次
sum =(1
+n)*n/2;
//執行1次
第一種演算法執行了1+(n+1)+n = 2n+2次。第二種演算法執行了1+1 = 2次。
如果我們將迴圈看作乙個整體,忽略頭尾判斷的語句,那麼這兩個演算法其實就是n和1的差距。因為迴圈判斷在演算法1裡面執行力n+1次,當n的值越大,迴圈的次數越多,消耗的時間就越長,而如果演算法執行一次,那麼不論 n 值有多大,演算法只執行一次,與 n 值無關。
再舉個例子:
int i, j, x=0, sum=0, n=100;
for( i=1; i <= n; i++)
}
在這個例子中,迴圈條件 i 從 1 到 100 ,每次都要讓j迴圈 100 次,總共迴圈 100^2 (即100*100) 次,由此可見,當數值較大時,我們不易使用迴圈來進行演算法處理。
我們研究演算法的複雜度,側重的是研究演算法隨著輸入規模擴大增長量的乙個抽象,而不是精確的定位需要執行多少次。我們在分析演算法的執行時間時,重要的是把基本操作的數量和輸入模式關聯起來。
在這裡我們將引入「函式的漸進增長」對演算法執行的效率進行判斷。
函式的漸進增長:給定兩個函式 f(n) 和 g(n) ,如果存在乙個整數 n ,使得對於所有的 n > n ,f(n) 總是比 g(n) 大, 那麼, 我們說 f(n) 的增長漸近快於 g(n) 。
我們常常在判斷乙個演算法的效率時,函式中的常數和其他次要項常常可以忽略,而更應該關注主項(最高項)的階數。
注意:判斷乙個演算法好不好,我們只通過少量的資料是不可以做出準確判斷的,很容易以偏概全。
在進行演算法分析時,語句總的執行次數 t(n) 是關於問題規模 n 的函式,進而分析 t(n) 隨 n 的變化情況並確定 t(n) 的數量級。演算法的時間複雜度,也就是演算法的時間量度,記作:t(n) = o( f(n) )。它表示隨問題規模 n 的增大,演算法執行時間的增長率和 f(n) 的增長率相同,稱作演算法的漸近時間複雜度,簡稱為時間複雜度。其中 f(n) 是問題規模 n 的某個函式。
其中關鍵是:執行次數 == 時間。
一般情況下,隨著輸入規模n的增大, t(n)增長最慢的演算法為最優演算法。
如何分析乙個演算法的時間複雜度呢?
//常數階
int sum = 0, n = 100;
printf("i love study!\n");
printf("i love study!\n");
printf("i love study!\n");
sum = (1+n)*n/2;
大家覺得這段**的時間複雜度為多少呢?
首先,我們看過時間複雜度的定義知道「 t(n) 是關於問題規模 n 的函式」,那麼,我們只用看關於 n 的語句就可以了
在上述**中,只有sum = (1+n)*n/2;
是關於 n 的語句,由於他只執行了一次,所以這段**的時間複雜度為 o(1)。
2、線性階
一般含有非巢狀迴圈涉及線性階,線性階就是隨著問題規模 n 的擴大,對應計算次數呈直線增長。
int i , n = 100, sum = 0;
for( i=0 ; i上面這段**,它的迴圈的時間複雜度為o(n),因為迴圈體中的**需要執行 n 次。
3、平方階
int i, j, n =
100;
for( i =
0; i < n; i++
)}
這段**中 n 等於100,也就是說外層迴圈每執行一次,內層迴圈就執行100次,那總共程式想要從這兩個迴圈出來,需要執行100*100次,也就是 n 的平方。所以這段**的時間複雜度為o(n^2)。
int i, j, n =
100;
for( i =
0; i < n; i++
)}
這段**就和上面的**有所不同了,這樣的該如何求解呢?
分析一下,由於當 i =0 時,內迴圈執行了 n 次,當 i = 1 時,內迴圈則執行 n - 1次 . . . . . . . 當 i = n -1 時,內迴圈執行 1 次,所以總的執行次數應該是:n+(n-1)+(n-2)+…+1=n(n+1)/2
繼續推算:n(n+1)/2 = n^2/2+n/2。
按照我們上述的計算方法,第一條忽略,因為沒有常數相加。第二條只保留最高項,所以n/2這項去掉。第三條,去除與最高項相乘的常數,最終得o(n^2)。
4、對數階
int i =
1, n =
100;
while
( i < n )
由於每次 i*2 之後,就距離 n 更近一步,假設有 x 個2相乘後大於或等於 n,則會退出迴圈。於是由 2^x = n 得到 x = log(2)n,所以這個迴圈的時間複雜度為o(logn)。
上面講述的**內容,其實很容易,我們現在把問題實際化一點,大家看看是否可以正確的分析出來。
int i, j;
for( i =
0; i < n; i++
)void
function
(int count)
函式體是列印這個引數,這很好理解。function函式的時間複雜度是o(1),所以整體的時間複雜度就是迴圈的次數o(n)。
假如function是下面這樣,又該如何呢?
void
function
(int count)
}
事實上,這和上面的平方階是一樣的,function內部的迴圈次數隨count的增加(接近n)而減少,所以根據上面的推理步驟,該**的演算法時間複雜度為o(n^2)。
n++
;//執行了 1 次
function
(n);
//執行了 n^2 次
for( i =
0; i)for
( i=
0; i < n; i++)}
void
function
(int count)
}
相加得:3*(n^2) + 1,出去常數,保留最高端,即得該演算法的時間複雜度為o( n ^ 2)。
常見的時間複雜度
例子時間複雜度
999666
o(1)
3n + 4
o(n)
3n^2+4n+5
o(n^2)
3log(2)n+4
o(logn)
2n+3nlog(2)n+14
o(nlogn)
n^3 + 2 n ^2+4n+6
o(n^3)
2^no(2^n)
常用的時間複雜度所耗費的時間從小到大依次是:o(1) < o(logn) < o(n) < o(nlogn) < o(n^2) < o( n ^3) < o(2 ^n) < o(n!) < o(n ^n)。
我們在寫**時,完全可以用空間來換取時間。
演算法的空間複雜度通過計算演算法所需的儲存空間實現,演算法的空間複雜度的計算公式記作:s(n) = o(f(n)),其中,n 為問題的規模,f(n)為語句關於 n 所佔儲存空間的函式。
通常來說,用「時間複雜度」來指執行時間的需求,用「空漸漸複雜度」來指空間需求。當直接要求我們求「複雜度」的時候,通常是指是時間複雜度。
我們下章的筆記將會是:資料結構——線性表。
上班第二天
本想坐晚一班車的,誰知道這晚一班的也只是晚了 五 六分鐘,不高興再在站台上等,也只好小跑到車門上去了,果然,八點十分就到了。白天那個困啊 今天的工作內容和昨天一樣,做到後來我實在覺得測的差不多了,想幹些其他的,但這初來乍到的,也不好幹其他的呀,況且領導辦公室就在我旁邊。今天的工作總結有兩點 1 別把...
第二天實習
今天我早早到了銀行,開始給大家抹桌子,算是向愛妃學習吧。之後向wqy學習做表的方法,他很耐心。今天一天也就是做了這麼乙個表,但是還向dh學習了另外一些業務的處理,收穫不小。中午吃了雞腿 肉串和芸豆,好開心啊。我越來越喜歡自己的實習了。今天好多自己班上的同學來面試實習生,希望她們都能上!今天我沒有去同...
實習第二天
又是早起擠地鐵的一天 今天symphony說給我換了個導師,richard。於是我開始自己看,但是我並不清楚背景,不知道整個資料夾是幹什麼目的的,也不知道哪些檔案是需要看的。然後在richard的講解下,我慢慢明白了,終於知道我是幹啥的了,就是完善優化他們寫的自動化測試程式,首先是要看懂他們寫的。按...