數(shù)據(jù)結(jié)構(gòu)——《大話數(shù)據(jù)結(jié)構(gòu)》之時間復(fù)雜度

聲明:該文章中內(nèi)容為《大話數(shù)據(jù)機構(gòu)》一書中的內(nèi)容

1 函數(shù)的漸近增長

我們現(xiàn)在來判斷一下,兩個算法A和B哪個更好。假設(shè)兩個算法的輸入規(guī)模都是n,算法A要做2n + 3次操作,你可以理解為先有一個n次的循環(huán),執(zhí)行完成后,再有一個n次循環(huán),最后有三次賦值或運算,共2n + 3次操作。算法B要做3n + 1次操作。你覺得它們誰更快呢?

準(zhǔn)確說來,答案是不一定的(如表2-8-1所示)。

當(dāng)n = 1時,算法A效率不如算法B(次數(shù)比算法B要多一次)。而當(dāng)n = 2時,兩者效率相同;當(dāng)n > 2時,算法A就開始優(yōu)于算法B了,隨著n的增加,算法A要越來越好過算法B了(執(zhí)行的次數(shù)比B要少)。于是我們可以得出結(jié)論,算法A總體上要好過算法B。

此時我們給出這樣的定義,輸入規(guī)模n在沒有限制的情況下,只要超過一個數(shù)值N,這個函數(shù)就總是大于另一個函數(shù)。我們稱函數(shù)是漸近增長的。

函數(shù)的漸近增長:給定兩個函數(shù)f(n)和g(n),如果存在一個整數(shù)N,使得對于所有的n > N,f(n)總是比g(n)大,那么,我們說f(n)的增長漸近快于g(n)。

從中我們發(fā)現(xiàn),隨著n的增大,后面的 +3還是 +1其實是不影響最終的算法變化的,例如算法A’與算法B’。所以,我們可以忽略這些加法常數(shù)。后面的例子,這樣的常數(shù)被忽略的意義可能會更加明顯。

我們來看第二個例子。算法C是4n + 8,算法D是2n2+ 1(如表2-8-2所示)。

當(dāng)n < = 3的時候,算法C要差于算法D(因為算法C次數(shù)比較多),但當(dāng)n > 3后,算法C的優(yōu)勢就越來越優(yōu)于算法D了,到后來更是遠(yuǎn)遠(yuǎn)勝過。而當(dāng)后面的常數(shù)去掉后,我們發(fā)現(xiàn)其實結(jié)果沒有發(fā)生改變。甚至我們再觀察發(fā)現(xiàn),哪怕去掉與n相乘的常數(shù),這樣的結(jié)果也沒發(fā)生改變,算法C’ 的次數(shù)隨著n的增長,還是遠(yuǎn)小于算法D’。也就是說,與最高次項相乘的常數(shù)并不重要。

我們再來看第三個例子。算法E是2n2+ 3n + 1,算法F是2n3+ 3n + 1(如表2-8-3所示)。

當(dāng)n = 1的時候,算法E與算法F結(jié)果相同,但當(dāng)n > 1后,算法E的優(yōu)勢就要開始優(yōu)于算法F,隨著n的增大,差異非常明顯。通過觀察發(fā)現(xiàn),最高次項的指數(shù)大的,函數(shù)隨著n的增長,結(jié)果也會變得增長特別快。

我們來看最后一個例子。算法G是2n2,算法H是3n + 1,算法I是2n2+ 3n + 1(如表2-8-4所示)。

這組數(shù)據(jù)應(yīng)該就看得很清楚。當(dāng)n的值越來越大時,你會發(fā)現(xiàn),3n+1已經(jīng)沒法和2n2的結(jié)果相比較,最終幾乎可以忽略不計。也就是說,隨著n值變得非常大以后,算法G其實已經(jīng)很趨近于算法I。于是我們可以得到這樣一個結(jié)論,判斷一個算法的效率時,函數(shù)中的常數(shù)和其他次要項常??梢院雎?,而更應(yīng)該關(guān)注主項(最高階項)的階數(shù)。

判斷一個算法好不好,我們只通過少量的數(shù)據(jù)是不能做出準(zhǔn)確判斷的。根據(jù)剛才的幾個樣例,我們發(fā)現(xiàn),如果我們可以對比這幾個算法的關(guān)鍵執(zhí)行次數(shù)函數(shù)的漸近增長性,基本就可以分析出,某個算法,隨著n的增大,它會越來越優(yōu)于另一算法,或者越來越差于另一算法。這其實就是事前估算方法的理論依據(jù),通過算法時間復(fù)雜度來估算算法時間效率。

2 算法的時間復(fù)雜度

2.1?算法時間復(fù)雜度定義

在進(jìn)行算法分析時,語句總的執(zhí)行次數(shù)T(n)是關(guān)于問題規(guī)模n的函數(shù),進(jìn)而分析T(n)隨n的變化情況并確定T(n)的數(shù)量級。算法的時間復(fù)雜度,也就是算法的時間量度,記作:T(n) = O(f(n))。它表示隨問題規(guī)模n的增大,算法執(zhí)行時間的增長率和f(n)的增長率相同,稱作算法的漸近時間復(fù)雜度,簡稱為時間復(fù)雜度。其中f(n)是問題規(guī)模n的某個函數(shù)。

這樣用大寫O()來體現(xiàn)算法時間復(fù)雜度的記法,我們稱之為大O記法。

一般情況下,隨著n的增大,T(n)增長最慢的算法為最優(yōu)算法。

顯然,由此算法時間復(fù)雜度的定義可知,我們的三個求和算法的時間復(fù)雜度分別為O(n),O(1),O(n2)。我們分別給它們?nèi)×朔枪俜降拿Q,O(1)叫常數(shù)階,O(n)叫線性階,O(n2)叫平方階,當(dāng)然,還有其他的一些階,我們之后會介紹。

2.2?推導(dǎo)大O階方法

那么如何分析一個算法的時間復(fù)雜度呢?即如何推導(dǎo)大O階呢?我們給出了下面的推導(dǎo)方法,基本上,這也就是總結(jié)前面我們舉的例子

推導(dǎo)大O階

1.用常數(shù)1取代運行時間中的所有加法常數(shù)。

2.在修改后的運行次數(shù)函數(shù)中,只保留最高階項。

3.如果最高階項存在且不是1,則去除與這個項相乘的常數(shù)。

得到的結(jié)果就是大O階。

哈,仿佛是得到了游戲攻略一樣,我們好像已經(jīng)得到了一個推導(dǎo)算法時間復(fù)雜度的萬能公式??墒聦嵣?,分析一個算法的時間復(fù)雜度,沒有這么簡單,我們還需要多看幾個例子。

2.3?常數(shù)階

首先順序結(jié)構(gòu)的時間復(fù)雜度。下面這個算法,也就是剛才的第二種算法,為什么時間復(fù)雜度不是O(3),而是O(1)。

intsum=0,n=100;/*執(zhí)行一次*/

sum=(1+n)*n/2;/*執(zhí)行一次*/

printf("%d",?sum);/*執(zhí)行一次*/

這個算法的運行次數(shù)函數(shù)是f(n)=3。根據(jù)我們推導(dǎo)大O階的方法,第一步就是把常數(shù)項3改為1。在保留最高階項時發(fā)現(xiàn),它根本沒有最高階項,所以這個算法的時間復(fù)雜度為O(1)。

另外,我們試想一下,如果這個算法當(dāng)中的語句sum=(1+n)*n/2有10句,即:

intsum=0,?n=100;/*執(zhí)行一次*/

sum=(1+n)*n/2;/*執(zhí)行第1次*/

sum=(1+n)*n/2;/*執(zhí)行第2次*/

sum=(1+n)*n/2;/*執(zhí)行第3次*/

sum=(1+n)*n/2;/*執(zhí)行第4次*/

sum=(1+n)*n/2;/*執(zhí)行第5次*/

sum=(1+n)*n/2;/*執(zhí)行第6次*/

sum=(1+n)*n/2;/*執(zhí)行第7次*/

sum=(1+n)*n/2;/*執(zhí)行第8次*/

sum=(1+n)*n/2;/*執(zhí)行第9次*/

sum=(1+n)*n/2;/*執(zhí)行第10次*/

printf("%d",sum);/*執(zhí)行一次*/

事實上無論n為多少,上面的兩段代碼就是3次和12次執(zhí)行的差異,這種與問題的大小無關(guān)(n的多少),執(zhí)行時間恒定的算法,我們稱之為具有O(1)的時間復(fù)雜度,又叫常數(shù)階。

注意,不管這個常數(shù)是多少,我們都記作O(1),而不能是O(3)、O(12)等其他任何數(shù)字。這是初學(xué)者常常犯的錯誤。

對于分支結(jié)構(gòu)而言,無論是真,還是假,執(zhí)行的次數(shù)都是恒定的,不會隨著n的變大而發(fā)生變化,所以單純的分支結(jié)構(gòu)(不包含在循環(huán)結(jié)構(gòu)中),其時間復(fù)雜度也是O(1)。

2.4?線性階

循環(huán)結(jié)構(gòu)就會復(fù)雜很多。要確定某個算法的階次,我們常常需要確定某個特定語句或某個語句集運行的次數(shù)。因此,我們要分析算法的復(fù)雜度,關(guān)鍵就是要分析循環(huán)結(jié)構(gòu)的運行情況。

下面這段代碼,它的循環(huán)的時間復(fù)雜度為O(n)。因為循環(huán)體中的代碼須要執(zhí)行n次。

inti;

for(i=0;?i

{

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

2.5?對數(shù)階

那么下面的這段代碼,時間復(fù)雜度又是多少呢?

intcount=1;

while(count

{

count=count*2;

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

由于每次count乘以2之后,就距離n更近了一分。也就是說,有多少個2相乘后大于n,則會退出循環(huán)。由2x=n得到x=log2n。所以這個循環(huán)的時間復(fù)雜度為O(logn)。

2.6?平方階

下面的例子是一個循環(huán)嵌套,它的內(nèi)循環(huán)剛才我們已經(jīng)分析過,時間復(fù)雜度為O(n)。

inti,j;

for(i=0;?i

{

for(j=0;?j

{

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

}

而對于外層的循環(huán),不過是內(nèi)部這個時間復(fù)雜度為O(n)的語句,再循環(huán)n次。所以這段代碼的時間復(fù)雜度為O(n2)。

如果外循環(huán)的循環(huán)次數(shù)改為了m,時間復(fù)雜度就變?yōu)镺(m×n)。

inti,j;

for(i=0;?i

{

for(j=0;?j

{

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

}

所以我們可以總結(jié)得出,循環(huán)的時間復(fù)雜度等于循環(huán)體的復(fù)雜度乘以該循環(huán)運行的次數(shù)。

那么下面這個循環(huán)嵌套,它的時間復(fù)雜度是多少呢?

inti,j;

for(i=0;?i

{

for(j=i;?j

{

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

}

由于當(dāng)i = 0時,內(nèi)循環(huán)執(zhí)行了n次,當(dāng)i = 1時,執(zhí)行了n-1次,……當(dāng)i = n-1時,內(nèi)循環(huán)執(zhí)行了1次。所以總的執(zhí)行次數(shù)為

用我們推導(dǎo)大O階的方法,第一條,沒有加法常數(shù)不予考慮;第二條,只保留最高階項,因此保留n2/2;第三條,去除這個項相乘的常數(shù),也就是去除1/2,最終這段代碼的時間復(fù)雜度為O(n2)。

從這個例子,我們也可以得到一個經(jīng)驗,其實理解大O推導(dǎo)不算難,難的是對數(shù)列的一些相關(guān)運算,這更多的是考察你的數(shù)學(xué)知識和能力,所以想考研的朋友,要想在求算法時間復(fù)雜度這里不失分,可能需要強化你的數(shù)學(xué),特別是數(shù)列方面的知識和解題能力。

我們繼續(xù)看例子,對于方法調(diào)用的時間復(fù)雜度又如何分析。

inti,j;

for(i=0;?i

{

function(i);

}

上面這段代碼調(diào)用一個函數(shù)function。

voidfunction(intcount)

{

print(count);

}

函數(shù)體是打印這個參數(shù)。其實這很好理解,function函數(shù)的時間復(fù)雜度是O(1)。所以整體的時間復(fù)雜度為O(n)。

假如function是下面這樣的:

voidfunction(intcount)

{

intj;

for(j=count;?j

{

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

}

事實上,這和剛才舉的例子是一樣的,只不過把嵌套內(nèi)循環(huán)放到了函數(shù)中,所以最終的時間復(fù)雜度為O(n2)。

下面這段相對復(fù)雜的語句:

n++;/*執(zhí)行次數(shù)為1*/

function(n);/*執(zhí)行次數(shù)為n*/

inti,j;

for(i=0;?i

{

function?(i);

}

for(i=0;?i

{

for(j=i;j

{

/*時間復(fù)雜度為O(1)的程序步驟序列*/

}

}

它的執(zhí)行次數(shù)

,根據(jù)推導(dǎo)大O階的方法,最終這段代碼的時間復(fù)雜度也是O(n2)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容