【数据结构与算法-java实现】一 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
今天開始學(xué)習(xí)程序的靈魂:數(shù)據(jù)結(jié)構(gòu)與算法。
本文是自己學(xué)習(xí)極客時間專欄-數(shù)據(jù)結(jié)構(gòu)與算法之美后的筆記總結(jié)。如有侵權(quán)請聯(lián)系我刪除文章。
我們都知道,數(shù)據(jù)結(jié)構(gòu)和算法本身解決的是“快”和“省”的問題,即如何讓代碼運行得更快,如何讓代碼更省存儲空間。所以,執(zhí)行效率是算法一個非常重要的考量指標(biāo)。那如何來衡量你編寫的算法代碼的執(zhí)行效率呢?這里就要用到我們今天要講的內(nèi)容:時間、空間復(fù)雜度分析。
復(fù)雜度分析是整個算法學(xué)習(xí)的精髓,只要掌握了它,數(shù)據(jù)結(jié)構(gòu)和算法的內(nèi)容基本上就掌握了一半。
1、大O復(fù)雜度表示法
算法的執(zhí)行效率,粗略的說,就是代碼的執(zhí)行時間。但是實際上代碼在被CPU執(zhí)行的時候,是相當(dāng)快的,這個時間我們也無法計算。所以就抽象出了一個用肉眼能夠看到的時間。以例子來分析,看如下一個求和的代碼:
int cal(int n) {int sum = 0;int i = 1;for (; i <= n; ++i) {sum = sum + i;}return sum;}對于CPU來說,它只知道從內(nèi)存中取指令與執(zhí)行指令。所以上述代碼,CPU就是一條一條的取指令與執(zhí)行該指令。現(xiàn)在假設(shè)CPU執(zhí)行每一條的指令的時間都是一樣的為:p_time。那么上述代碼第二三行執(zhí)行時間2*p_time,四五六行是一個循環(huán)。所以第四五行的執(zhí)行時間是2n*p_time。所以總的執(zhí)行時間是: (2n+2)*p_time
可以看到:所有代碼的執(zhí)行時間 T(n) 與每行代碼的執(zhí)行次數(shù)成正比。
按照上述思路,我們再來分析以下代碼:
int cal(int n) {int sum = 0;int i = 1;int j = 1;for (; i <= n; ++i) {j = 1;for (; j <= n; ++j) {sum = sum + i * j;}}}整段代碼總的執(zhí)行時間 T(n) = (2n2+2n+3)*p_time。
這里我們雖然不知道p_time的具體值,但是很明顯,代碼的總的執(zhí)行時間T(n)是與n成正比(這里的正比,不是數(shù)學(xué)的正比,是隨著n的增大,T(n)越來越大)。
我們可以把這種規(guī)律,總結(jié)成一個規(guī)律。此時大O就出現(xiàn)了。
T(n)=O(f(n))T(n) 表示代碼執(zhí)行的時間;n 表示數(shù)據(jù)規(guī)模的大小;f(n) 表示每行代碼執(zhí)行的次數(shù)總和。因為這是一個公式,所以用 f(n) 來表示。公式中的 O,表示代碼的執(zhí)行時間 T(n) 與 f(n) 表達式成正比。
所以,第一個例子中的 T(n) = O(2n+2),第二個例子中的 T(n) = O(2n2+2n+3)。這就是大 O 時間復(fù)雜度表示法。
注意:大O時間復(fù)雜度表示法,并不代表代碼的真正執(zhí)行時間,而是代表執(zhí)行的時間隨數(shù)據(jù)規(guī)模增長的一種變化趨勢。 簡稱時間復(fù)雜度。
為了簡化時間復(fù)雜度的表示,以及由于一些常數(shù),系數(shù)以及量級比較小的項對整體的變化影響不大,所以一般將他們?nèi)サ簟D敲匆陨蟽煞N例子的時間復(fù)雜度在簡化以后就是:T(n)=O(n)和T(n)=O(n2)。
2、時間復(fù)雜度分析
遇到一段代碼,如何分析它的時間復(fù)雜度。一般有三種方法
2.1 只關(guān)注循環(huán)次數(shù)最多的一段代碼
例如如下代碼:
int cal(int n) {int sum = 0;int i = 1;for (; i <= n; ++i) {sum = sum + i;}return sum;}它的時間復(fù)雜度就位:T(n)=O(n)
2.2、加法法則
總的時間復(fù)雜度等于量級最大的那段代碼的時間復(fù)雜度。
看如下代碼:
int cal(int n) {int sum_1 = 0;int p = 1;for (; p < 100; ++p) {sum_1 = sum_1 + p;}int sum_2 = 0;int q = 1;for (; q < n; ++q) {sum_2 = sum_2 + q;}int sum_3 = 0;int i = 1;int j = 1;for (; i <= n; ++i) {j = 1; for (; j <= n; ++j) {sum_3 = sum_3 + i * j;}}return sum_1 + sum_2 + sum_3;}上述代碼中,第一段代碼的循環(huán)為100次,第二段代碼的循環(huán)為n次,第三段代碼的時間復(fù)雜度為n2次。此時,要注意一點,任何常數(shù)次循環(huán),都是O(1時間復(fù)雜度),不管是100次,10000次,10000000次,只要能看出是一個具體的數(shù)字,它都是O(1)時間復(fù)雜度。
由總的時間復(fù)雜度等于量級最大的那段代碼的時間復(fù)雜度。所以上述代碼最終時間復(fù)雜度為O(n2)
結(jié)論:
如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).
2.3、乘法法則
嵌套代碼的復(fù)雜度等于嵌套內(nèi)外代碼復(fù)雜度的乘積
上面講了一個復(fù)雜度分析中的加法法則,這兒還有一個乘法法則。類比一下,你應(yīng)該能“猜到”公式是什么樣子的吧?
如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)).
也就是說,假設(shè) T1(n) = O(n),T2(n) = O(n2),則 T1(n) * T2(n) = O(n3)。落實到具體的代碼上,我們可以把乘法法則看成是嵌套循環(huán),舉個例子給你解釋一下。
int cal(int n) {int ret = 0; int i = 1;for (; i < n; ++i) {ret = ret + f(i);} } int f(int n) {int sum = 0;int i = 1;for (; i < n; ++i) {sum = sum + i;} return sum;}我們單獨看 cal() 函數(shù)。假設(shè) f() 只是一個普通的操作,那第 4~6 行的時間復(fù)雜度就是,T1(n) = O(n)。但 f() 函數(shù)本身不是一個簡單的操作,它的時間復(fù)雜度是 T2(n) = O(n),所以,整個 cal() 函數(shù)的時間復(fù)雜度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n2)。
3、幾種常見復(fù)雜度分析
3.1、O(1)時間復(fù)雜度
如果代碼中沒有循環(huán)或者循環(huán)的次數(shù)是可以確定的常數(shù),那么就是O(1)復(fù)雜度
3.2、O(logn) O(nlogn)
看下面的代碼:
i=1;while (i <= n) {i = i * 2;}設(shè)執(zhí)行的次數(shù)為x,則2x=n。解得x=log2n
再看下面的代碼:
i=1;while (i <= n) {i = i * 3;}設(shè)執(zhí)行的次數(shù)為x,3x=n。解得x=log3n
實際上,不管是以 2 為底、以 3 為底,還是以 10 為底,我們可以把所有對數(shù)階的時間復(fù)雜度都記為 O(logn)。為什么呢?
我們知道,對數(shù)之間是可以互相轉(zhuǎn)換的,log3n 就等于 log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一個常量。基于我們前面的一個理論:在采用大 O 標(biāo)記復(fù)雜度的時候,可以忽略系數(shù),即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在對數(shù)階時間復(fù)雜度的表示方法里,我們忽略對數(shù)的“底”,統(tǒng)一表示為 O(logn)。
如果你理解了前面講的 O(logn),那 O(nlogn) 就很容易理解了。還記得剛講的乘法法則嗎?如果一段代碼的時間復(fù)雜度是 O(logn),我們循環(huán)執(zhí)行 n 遍,時間復(fù)雜度就是 O(nlogn) 了。而且,O(nlogn) 也是一種非常常見的算法時間復(fù)雜度。比如,歸并排序、快速排序的時間復(fù)雜度都是 O(nlogn)。
3.1、O(m+n)、O(m*n)
看如下代碼:
int cal(int m, int n) {int sum_1 = 0;int i = 1;for (; i < m; ++i) {sum_1 = sum_1 + i;}int sum_2 = 0;int j = 1;for (; j < n; ++j) {sum_2 = sum_2 + j;}return sum_1 + sum_2; }從代碼中可以看出,m 和 n 是表示兩個數(shù)據(jù)規(guī)模。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示復(fù)雜度的時候,就不能簡單地利用加法法則,省略掉其中一個。所以,上面代碼的時間復(fù)雜度就是 O(m+n)。
針對這種情況,原來的加法法則就不正確了,我們需要將加法規(guī)則改為:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法則繼續(xù)有效:T1(m)*T2(n) = O(f(m) * f(n))。
4、空間復(fù)雜度的分析
前面,花了很長時間講大 O 表示法和時間復(fù)雜度分析,理解了前面講的內(nèi)容,空間復(fù)雜度分析方法學(xué)起來就非常簡單了。
前面我講過,時間復(fù)雜度表示算法的執(zhí)行時間與數(shù)據(jù)規(guī)模之間的增長關(guān)系。類比一下,空間復(fù)雜度表示算法的存儲空間與數(shù)據(jù)規(guī)模之間的增長關(guān)系。
看如下代碼:
void print(int n) {int i = 0;int[] a = new int[n];for (i; i <n; ++i) {a[i] = i * i;}for (i = n-1; i >= 0; --i) {print out a[i]} }跟時間復(fù)雜度分析一樣,我們可以看到,第 2 行代碼中,我們申請了一個空間存儲變量 i,但是它是常量階的,跟數(shù)據(jù)規(guī)模 n 沒有關(guān)系,所以我們可以忽略。第 3 行申請了一個大小為 n 的 int 類型數(shù)組,除此之外,剩下的代碼都沒有占用更多的空間,所以整段代碼的空間復(fù)雜度就是 O(n)。
我們常見的空間復(fù)雜度就是 O(1)、O(n)、O(n2 ),像 O(logn)、O(nlogn) 這樣的對數(shù)階復(fù)雜度平時都用不到。而且,空間復(fù)雜度分析比時間復(fù)雜度分析要簡單很多。
5、總結(jié)
復(fù)雜度也叫漸進復(fù)雜度,包括時間復(fù)雜度和空間復(fù)雜度,用來分析算法執(zhí)行效率與數(shù)據(jù)規(guī)模之間的增長關(guān)系,可以粗略地表示,越高階復(fù)雜度的算法,執(zhí)行效率越低。常見的復(fù)雜度并不多,從低階到高階有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )。
本文是自己學(xué)習(xí)極客時間專欄-數(shù)據(jù)結(jié)構(gòu)與算法之美后的筆記總結(jié)。如有侵權(quán)請聯(lián)系我刪除文章。
學(xué)習(xí)探討加個人:
qq:1126137994
微信:liu1126137994
總結(jié)
以上是生活随笔為你收集整理的【数据结构与算法-java实现】一 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北京林业大学计算机技术复试,北京林业大学
- 下一篇: android 支付宝 记账本,使用支付