MIT算法导论(一)——算法分析和引论
文章目錄
- 1 算法分析及引論
- 1.1 算法
- 1.2 排序
- 1.2.1 插入排序
- 1.2.1.1 插入排序原理
- 1.2.1.2 時間復(fù)雜度
- 1.2.1.3 漸進(jìn)時間復(fù)雜度
- 1.2.1.4 回到算法
- 1.2.2 歸并排序
- 1.2.2.1 歸并排序原理
- 1.2.2.2 歸并排序時間復(fù)雜度
1 算法分析及引論
1.1 算法
算法是一門關(guān)注性能的學(xué)科,也就是說,我們致力于讓我們要做的事變得更快。讓我們提出一個問題,在程序設(shè)計(jì)中,什么比性能更重要?
成本(程序員的時間成本),可維護(hù)性,穩(wěn)定性(健壯性,是否老崩潰),功能性,安全,可擴(kuò)展性。
既然這么多因素都比性能更重要,那我們?yōu)楹芜€要研究算法,到底是性能重要還是用戶的體驗(yàn)更重要呢?
可以肯定的是,很多時候性能和用戶體驗(yàn)是緊密聯(lián)系在一起的,很多時候沒有什么東西比干等著更難受了。
我們研究算法的緣由是:很多時候性能的好壞決定其是否可行,如我們對實(shí)時的要求,如果算法性能不快,那么就會導(dǎo)致無法追求實(shí)時的效果,實(shí)時當(dāng)然就不可行了。或者說,如果它占用過多的內(nèi)存,它也是不可行的,因?yàn)橛布枨蟾簧稀?偟膩碚f:算法走在所有因素的最前沿
第二個關(guān)鍵是,算法是一種解釋性語言。其通用讓幾乎所有程序員都接收,它是一種讓程序最為簡潔的思考方式。我們有一個很好說明的例子:我們把算法比作貨幣,那么貨幣作為社會最底層的東西,如果你想要買吃的,買住的,都離不開貨幣。
第三個關(guān)鍵是,算法決定了一個程序的安全和用戶體驗(yàn)的好壞。雖然C比Java快很多,java編程性能會損失三倍左右,但是人們還是喜歡用java來編寫程序,因?yàn)閖ava的面向?qū)ο髾C(jī)制以及異常機(jī)制值得人們?yōu)橹冻觥_@也是為什么我們要提高性能,因?yàn)槲覀兛偸前研阅芊謹(jǐn)偨o其他的要素。
第四個關(guān)鍵是,我們對此樂此不疲。人們總是喜歡快的東西,比如火箭、滑雪,我們總喜歡這些快速的東西。
在某種意義上,上述這些簡單的問題是我們研究下列問題的指路明燈。
1.2 排序
讓我們來看看一個非常古老的問題,排序。排序包含了許多基本的算法。讓我們舉一個排序的例子:我們輸入一組序列a1,a2...ana_1,a_2...a_na1?,a2?...an?,按照需求排列后作為輸出。排列一般是使得如a1<=a2...<=ana_1<=a_2...<=a_na1?<=a2?...<=an?一般。按一定順序排序。
1.2.1 插入排序
1.2.1.1 插入排序原理
這里我們介紹第一個算法,即插入排序。通常我們描述算法用的是偽代碼而不是編程語言。偽代碼中包含有一些解釋性語言如英語或者中文,其能夠讓我們更清楚該算法包含的思想。
值得一提的是,用偽代碼描述算法時,我們通常會使用縮進(jìn),其相當(dāng)于大多數(shù)語言中標(biāo)注開始和結(jié)束的分隔符。好比Java和C中的大括號。事實(shí)上,歷史上還真有一些語言(例如python)是用縮進(jìn)代表分隔,但是不是一種好的主義,因?yàn)樵趽Q頁的時候,你很難知道自己在哪一個嵌套的層級,而是用大括號就能夠很好的表示。
讓我們締造一個數(shù)組,如下所示:
這個數(shù)組通常是無序的,然而我們通過從從左到右指定一個變量,這個變量我們稱為鍵,他會從數(shù)組中提取數(shù)據(jù),然后和前面已經(jīng)排好的序列做比較,然后插入前面的序列,這也是為這么這個算法叫做插入排序的原因。
比較經(jīng)典的例子我認(rèn)為是斗地主,在發(fā)牌時,我們時常會事先排好手中的牌,我們是一張一張拿起來的,然后將亂序的牌提取出來,然后插入前面已經(jīng)排序好的位置中。無論什么時候,左手的牌都是排好序的,而右手拿的牌都是即將要插入到排好序的牌中,并且插入位置符合其自身順序。
我們來看一下插入排序的偽代碼實(shí)現(xiàn):
INSERTION-SORT(A)for j←2 to length[A]do key←A[j]Insert A[j] into the sorted sequence A[1..j-1]i←j-1while i>0 and A[i]>keydo A[i+1]←A[i]i←i-1A[i+1]←key看上面的偽代碼似乎有些費(fèi)勁,可事實(shí)上,它與我們上面講述的別無二致。讓我們引入一個具體的例子來體會這個算法。
我們有一個序列:8 2 4 9 3 6
當(dāng)我們執(zhí)行插入排序時,我們的首要任務(wù)是從第二個元素開始,依次提取鍵key,然后和前面排序好的序列做對比再插入。題意來看首先提取的是2,然后插入8之前。原序列則變?yōu)? 8 4 9 3 6。接著我們找到4,4和2 8進(jìn)行比對,然后插入2和8之間,原序列變?yōu)? 4 8 9 3 6,以此類推按照排序規(guī)則循環(huán)移動。
讓我們借助數(shù)學(xué)工具來解剖該算法。如果輸入序列之前就有序,那么已排序序列后的排序工作量就大大減少。因?yàn)槊看伪闅v,它都是做上面循環(huán)往復(fù)的事。最壞的情況是該序列剛好是逆序,那么每個元素都將進(jìn)行插入排序。
對應(yīng)在這道題上,這個例子總共有6個數(shù)據(jù),我們將其輸入的規(guī)模擴(kuò)大到100000,那么一旦發(fā)生逆序,其時間開銷很大。為此在進(jìn)行算法分析時,我們通常使用漸進(jìn)時間復(fù)雜度,即假設(shè)輸入的規(guī)模為n,趨于無限,則運(yùn)行時間可以看做是以n為映射的函數(shù)。
1.2.1.2 時間復(fù)雜度
上述引入的時間復(fù)雜度中,顯然有最好最壞的情況,最壞對應(yīng)算法時間的上限,其代表著對用戶的一種承諾,(是的尊貴的用戶我的程序是不會超過這個時間的)。而最好的情況對應(yīng)著算法時間的下限。我們通常關(guān)注算法的最壞時間復(fù)雜度。即我們要給出用戶保證,我們的程序總能做到這樣,而不是有時能這樣,我們通常會把輸入考慮成最壞的情況,對應(yīng)上面插入排序的例子,我們最壞的情況是逆序。
當(dāng)然有時我們也會討論平均時間復(fù)雜度,這個時候T(n)就成了我們算法的時間期望值。那不禁有人會問,期望值是啥?
我們希望聽到一些比較數(shù)學(xué)味道的回答,所謂的期望實(shí)際上就是每種輸入的運(yùn)行時間,乘以那種輸入出現(xiàn)的概率。這是一種連續(xù)型的思考方式,我們可以理解為加權(quán)平均。
那我們?nèi)绾沃捞囟ㄝ斎朐诮o定情況下的概率是多少?有時候我們并不知道,所以我們需要給出一個假設(shè)了,一個有關(guān)輸入的統(tǒng)計(jì)分布的假設(shè),否則期望時間無從談起。
最常見的假設(shè)是均勻分布,即所有規(guī)模為n的輸入情況都是等可能地出現(xiàn)。
最后我們想講講最好時間復(fù)雜度,我們稱之為假象!~bogus(我在大聲地笑哈哈),沒啥用!因?yàn)樽詈玫那闆r不可能出現(xiàn)。如果一個序列已經(jīng)排好序,我們還有必要去排序嗎,哈哈哈。除非你想要cheat!欺騙!在你得到一個特定輸入下,然后你得到一個比較滿意的結(jié)果后欺騙自己,對!這就是我想要的效果,那你就可以考慮它。
1.2.1.3 漸進(jìn)時間復(fù)雜度
回到這個算法,我們不禁發(fā)問,插入排序的最壞情況時間是多少?
最簡單的回答是,這取決于你的計(jì)算機(jī)。你用的是超級計(jì)算機(jī)還是腕表那么大的計(jì)算機(jī),算力會大相徑庭。但是通常來說,我們都是比對兩個算法在同一臺機(jī)器上的表現(xiàn),即相對速度。當(dāng)然也有人關(guān)注絕對速度,我們猜想真有某種算法能無論在什么機(jī)器上運(yùn)行都表現(xiàn)得更好嗎?
以上的回答實(shí)際上會對我們最開始的問題造成混淆,這不得不提到我們的大局觀(big idea)了,這也是為什么算法涉獵如此廣泛的原因。我們應(yīng)該使用抽象的眼光來看待事物,從復(fù)雜的情況中提取關(guān)鍵的因素對其分析。這就是所謂的漸進(jìn)分析。漸進(jìn)分析的基本思路是:忽略掉依賴于機(jī)器的常量,不去檢查其因素,而是關(guān)注算法本身時間的增長。
使用漸進(jìn)分析,自然要引入相應(yīng)的數(shù)學(xué)符號。這里我們使用的是thetaΘ\ThetaΘ來表示漸進(jìn)時間復(fù)雜度。實(shí)際上theta寫起來很簡單,你需要做的是,棄去它的低階項(xiàng),并忽略前面的常數(shù)因子。
比如一個算法花費(fèi)時間是:3n3+90n2?5n+60463n^3+90n^2-5n+60463n3+90n2?5n+6046,那我們就要使用高數(shù)中采取的抓大頭準(zhǔn)則,使的3n33n^33n3后面的項(xiàng)全部拋棄,然后再把3拋棄掉即可。所得為Θ(n3)\Theta(n^3)Θ(n3)。
上述情況需要知道的是,假設(shè)有一個算法的時間復(fù)雜度是Θ(n2)\Theta(n^2)Θ(n2),那么它遲早比Θ(n3)\Theta(n^3)Θ(n3)速度要快。因?yàn)楫?dāng)n逐漸增大時,指數(shù)會造成指數(shù)爆炸,使得比最大項(xiàng)還小的常數(shù)項(xiàng)無法動搖這個最終的結(jié)果。這對應(yīng)到計(jì)算機(jī)的優(yōu)劣上,即使你的Θ(n2)\Theta(n^2)Θ(n2)算法是在慢速計(jì)算機(jī)上運(yùn)行,總有一天它也會超越在快速計(jì)算機(jī)上運(yùn)行的Θ(n3)\Theta(n^3)Θ(n3)算法。
當(dāng)然,站在工程角度上看,n有時候太大沒有節(jié)制是不行的,這會導(dǎo)致我們的計(jì)算機(jī)無法運(yùn)行該算法,這也是為什么有時候我們對一些慢速算法比較感興趣,因?yàn)樗鼈冊趎較少的時候能夠保持較高的速度。所以僅僅會做算法分析并不能使你成為高手,你需要保持每天編程,并且在實(shí)際編程中運(yùn)用,知道其什么時候相關(guān)什么時候不相關(guān)。
1.2.1.4 回到算法
這時候我們可以來分析一些剛才的插入排序算法了。就像我們說的我們關(guān)注其最壞時間復(fù)雜度。即輸入為逆序。
INSERTION-SORT(A)for j←2 to length[A]do key←A[j]Insert A[j] into the sorted sequence A[1..j-1]i←j-1while i>0 and A[i]>keydo A[i+1]←A[i]i←i-1A[i+1]←key在這其中明顯有嵌套循環(huán),我們實(shí)際上關(guān)注循環(huán),因?yàn)槠渌?shù)操作無關(guān)緊要,我們是對其漸進(jìn)分析。第一個循環(huán)中是2到j(luò),而內(nèi)部循環(huán)是從無需的抽出元素對前面的有序序列做插入操作,實(shí)際上是由2到j(luò)次比對。也就是說,時間復(fù)雜度是θ(n2n^2n2)。
那這個時間復(fù)雜度到底快不快呢?對于小的n它確實(shí)挺快,但是對于巨大的n它就不是那么回事了,所以在下面我會給你一個更快的算法。
1.2.2 歸并排序
1.2.2.1 歸并排序原理
讓我們還是用抽象逐步講到具體。我們給出一個對于A[1…n]的歸并排序。
- 如果給定序列是1,自然不用排序,因?yàn)樾蛄兄兄挥幸粋€元素。
- 否則進(jìn)行遞歸,遞歸的每一層都會將序列一分為二,直到分出一個元素為止,然后對其一對一排序。
- 最后將排序后的序列全部重組。
這里我們出現(xiàn)一個新名詞——遞歸,這個知識點(diǎn)實(shí)際上有點(diǎn)小難,我推薦你去看一下我寫的博客數(shù)據(jù)結(jié)構(gòu)雜談番外篇——搞懂遞歸的小文章_弄鵲-CSDN博客。
上述歸并算法關(guān)鍵的部分在于歸并。歸并實(shí)際上是使用了分治法的思想,先將大問題分解成小問題,然后再將小問題求解后合并結(jié)果。
假設(shè)我現(xiàn)在有這么一個數(shù)組 8 4 5 7 1 3 6 2,如果采用歸并算法,我們是這么做的:
在我們使用遞歸進(jìn)入最深層次(即不可再分的第三層)時,我們開始進(jìn)行治,我們治的方式是用兩根數(shù)組指針分別指向分開的兩個序列,如下圖所示:
這個操作的時間復(fù)雜度是θ(n),因?yàn)槲覀兯〞r間都是在合并n次上。實(shí)際上分解并不耗費(fèi)時間,因?yàn)槊看芜f歸分解都是分解一次。而每次合并要移動指針。
1.2.2.2 歸并排序時間復(fù)雜度
讓我們來看看整個歸并排序的時間復(fù)雜度是多少。實(shí)際上歸并排序總時間=子序列排好序時間+合并時間。
現(xiàn)在我們提前使用遞歸樹方法來解決這里的問題,關(guān)于詳細(xì)我們會在下一小節(jié)敘述:
假設(shè)我們有n個數(shù)的序列,那么第一次分就可以分為兩個n/2的序列。
T(n)=2?T(n/2)+合并時間T(n) = 2*T(n/2)+合并時間T(n)=2?T(n/2)+合并時間。又由于合并的時候按照我們上面所說是循環(huán)比較兩個指針指向值的大小,所以復(fù)雜度為n。則我們可以改寫T(n)=2?T(n/2)+θ(n)T(n) = 2 *T(n/2)+θ(n)T(n)=2?T(n/2)+θ(n)。當(dāng)然了,對于n = 1的情況,T(n) = 1,這個情況我們一般忽略,因?yàn)樗麑f歸解沒有影響。我們用顯式的c?nc*nc?n來替換隱式的θ(n),然后把其寫出樹狀結(jié)構(gòu)如下所示:
c* n指的是多個常數(shù)階步驟所消耗的時間復(fù)雜度。常數(shù)階的時間復(fù)雜度在進(jìn)行漸進(jìn)表示時通常省略,所以我們說他是隱式的,而我們用顯式的c*n來表示這些步驟。因?yàn)槲覀冊谶@個時候是要計(jì)算T(n)而不是Θ(n)\Theta(n)Θ(n)。
也就是說,這個遞歸樹的每一層實(shí)際上都是cn。如第二層cn = T(n/2)+T(n/2)。那我們要計(jì)算遞歸樹中所有結(jié)點(diǎn)所消耗的時間復(fù)雜度,即全部相加,用cn乘上樹的高度即可。樹的高度為lgn,所以在遞歸樹中歸并算法花費(fèi)的時間復(fù)雜度為cn?lgncn*lgncn?lgn。這時候我們?nèi)绻鬂u進(jìn)時間復(fù)雜度,只需忽略常數(shù)項(xiàng),所以由此可得歸并排序時間復(fù)雜度為Θ(nlgn)\Theta(nlgn)Θ(nlgn)。
在計(jì)算機(jī)這一塊領(lǐng)域中,lgn就是log2nlog_2nlog2?n。我們設(shè)n除2分了一次,除2除了x次,那么最終n/2xn/2^xn/2x = 1,通過指對互換進(jìn)行反解,即x=log2nx = log_2nx=log2?n。也就是說樹的高度是log2nlog_2nlog2?n。而lgn只不過是log2nlog_2nlog2?n的常數(shù)倍,由漸進(jìn)表示可知這個不影響。所以你要拿lgn表示log3、log4nlog_3、log_4nlog3?、log4?n也是可以的。
總結(jié)
以上是生活随笔為你收集整理的MIT算法导论(一)——算法分析和引论的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: USB免驱NFC读写器 Android系
- 下一篇: 2009年最佳80后科技创业者