算法算法
算法是解決特定問題求解步驟的描述,在計算機中表現爲指令的有限序列,並且每條指令表示一個或多個操做。編程
算法設計的要求:應該具備正確性。可讀性、健壯性、高效率和低存儲量的特徵。小程序
你們都已經學過一門計算機語言,無論學的是哪種,學得好很差,好歹是能夠寫點小程序了。如今我要求你寫一個求 1+2+3+...+100結果的程序,你應該怎麼寫呢?數組
大多數人會立刻寫出下面的C語言代碼(或者其餘語言的代碼):框架
int i, sum=0, n=100; for (int i=1; i<=n; i++){ sum += i; } print("%d",sum);
這是最簡單的計算機程序之一,它就是一種算法,我不去解釋着代碼的含義了。問題在於,你的第一直覺是這樣寫的,單這樣是否是真的很好?是否是最高效?函數
而高斯的解法是: 工具
用程序來實現以下: 測試
神童就是神童,他用的方法至關於另外一種求等差數列的算法,不只僅能夠用於1加到100,就是加到一千、一萬、一億(須要更改整型變量類型爲長整型,不然會溢出),也就是瞬間之事。單若是用剛纔的程序,顯然計算機要循環一千、一萬、一億次的加法運算。人腦比電腦算得快,彷佛成爲了現實。操作系統
算法具備五個基本特性:輸入、輸出、有窮性,肯定性和可行性。設計
輸入和輸出特性比較容易理解,算法具備零個或多個輸入。儘管對於絕大多數算法來講,輸入參數都是必要的,但對於個別狀況,如打印"hello word!"這樣的代碼,不須要任何輸入參數,所以算法的輸入能夠是零個。算法至少有一個或多個輸出,算法是必定須要輸出的,不須要輸出,你用這個算法幹嘛?輸出的形式能夠是打印輸出,也能夠是返回一個或多個值等。
**有窮性:指算法在執行有限的步驟以後,自動結束而不會出現無限循環,而且每個步驟在可接受的時間內完成。**現實中常常會寫出死循環的代碼,這就是不知足有窮性。固然這裏有窮的概念並非純數學意義的,而是在實際應用當中合理的、能夠接受的"有邊界"。你說你寫一個算法,計算機須要算上個二十年,必定會結束,它在數學意義上是有窮了,能夠媳婦都熬成婆了,算法的意義也就不大了。
**肯定性:算法的每一步驟都具備肯定的含義,不會出現二義性。**算法在必定條件下,只有一條執行路徑,相同的輸入只能有惟一的輸出結果。算法的每一個步驟被精肯定義而無歧義。
**可行性:算法的每一步都必須是可行的,也就是說,每一步都可以經過執行有限次數完成。**可行性意味着算法能夠轉換爲程序上機運行,並獲得正確的結果。儘管目前計算機界也存在那種沒有實現的極爲複雜的算法,不是說理論上不能實現,而是由於過於複雜,咱們當前的編程方法、工具和大腦限制了這個工做,不過這都是理論研究領域的問題,不屬於咱們如今要考慮的範圍。
正確性:算法的正確性是指算法至少應該具備輸入、輸出和加工處理無歧義性,能正確反映問題的需求、可以獲得問題的正確答案。
可是算法的"正確"一般在用法上有很大的差異,大致分爲如下四個層次。
對於這四層含義,層次1要求最低,可是僅僅沒有語法錯誤實在談不上是好算法。這就如同僅僅解決溫飽,不能算是生活幸福同樣。而層次4是最困難的,咱們幾乎不可能逐一驗證全部的輸入都獲得正確的結果。
所以算法的正確性在大多數狀況下都不可能用程序來證實,而是用數學方法證實的。證實一個複雜算法在全部層次上都是正確的,代價很是昂貴。因此通常狀況下,咱們把層次3做爲一個算法是否正確的標準。
好算法還有什麼特徵呢?
可讀性:算法設計的另外一目的是爲了便於閱讀、理解和交流。
健壯性:當輸入數據不合法時,算法也能作出相關處理,而不是產生異常或莫名其妙的結果。
時間效率指的是算法的執行時間,對於同一個問題,若是有多個算法可以解決,執行時間短的算法效率高,執行時間長的效率低。存儲量需求指的是算法在執行過程當中須要的最大存儲空間,主要指算法程序運行時所佔用的內存或外部硬盤存儲空間。設計算法應該儘可能知足時間效率高和存儲量低的需求。在生活中,人們都但願花最少的錢,用最短的時間,辦最大的事,算法也是同樣的思想,最好用最少的存儲空間,花最少的時間,辦成統一的事就是好的算法。
過後統計方法:這種方法主要是經過設計好的測試程序和數據,利用計算機計時器對不一樣算法編制的程序的運行時間進行比較,從而肯定算法的效率的高低。
可是這種方法顯然是有很大缺陷的:
基於過後統計方法有這樣那樣的缺陷,咱們考慮不予採納。
事前分析估算方法:在計算機程序編制前,依據統計方法對算法進行估算。 拋開這些與計算機硬件、軟件有關的因素。一個程序的運行時間,依賴於算法的好壞和問題的輸入規模。所謂問題輸入規模是指輸入量的多少。
咱們來看看今天剛上課時舉的例子,兩種求和的算法: 顯然,第一種算法,執行了1+(n+1)+n+1次=2n+3次;而第二種算法,是1+1+1=3次。事實上兩個算法的第一條和最後一條語句是同樣的,因此咱們關注的代碼實際上是中間那部分,咱們把循環看做一個總體,忽略頭尾循環判斷的開銷,那麼這兩個算法其實就是n次和1次的差距。算法好壞顯而易見。
咱們再來延伸一下上面這個例子: 這個例子中,i從1到100,每次都要讓j循環100次,而當中的x++和sum=sum+x;其實就是1+2+3+...+10000,也就是100的平方次,因此這個算法當中,循環部分的代碼總體須要執行n的平方(忽略循環體頭尾的開銷)次。顯然這個算法的執行次數對於一樣的輸入規模n=100,要多於前面兩種算法,這個算法的執行時間隨着n的增長也將遠遠多於前面兩個。
此時你會看到,測定運行時間最可靠的方法就是計算對運行時間有消耗的基本操做的執行次數。運行時間和這個計數成正比。
咱們不關心編寫程序所用的程序設計語言是什麼,也不關心這些程序將跑在什麼樣的計算機中,咱們只關心它所實現的算法。這樣,不計那些循環索引的遞增和循環終止條件、變量聲明、打印結果等操做,最終,在分析程序的運行時間時,最重要是把程序當作是獨立於程序設計語言的算法或一系列步驟。
某個算法,隨着n的增加,它會愈來愈優於另外一算法,或者愈來愈差於另外一算法。這其實就是事前估算方法的理論依據,經過算法時間複雜度來估算算法時間效率。
在進行算法分析時,語句總的執行次數T(n)是關於問題規模n的函數,進行分析T(n)隨n的變化狀況並肯定T(n)的數量級。算法的時間複雜度,也就是算法的時間量度,記做:T(n)=O(f(n))。它表示隨問題規模n的增大,算法執行時間的增加率和f(n)的增加率相同,稱做算法的漸近時間複雜度,簡稱爲時間複雜度。其中f(n)是問題規模n的某個函數。
咱們要分析算法的複雜度,關鍵就是要分析循環結構的運行狀況。
首先順序結構的時間複雜度。下面這個算法,也就是剛纔的第二種算法(高斯算法),爲何時間複雜度不是O(3),而是O(1)。 這個算法的運行次數函數是f(n)=3.根據咱們推到大O階的方法,第一步就是把常數項3改成1.在保留最高階項時發現,它根本沒有最高階項,因此這個算法的時間複雜度爲O(1)。
另外,咱們試想一下,若是這個算法當中的語句 sum=(1+n)*n/2有10句,即:
事實上不管n爲多少,上面的兩段代碼就是3次和12次執行的差別。這種與問題的大小無關(n的多少),執行時間恆定的算法,咱們稱之爲具備O(1)的時間複雜度,又叫常數階。
注意:無論這個常數是多少,咱們都記做O(1),而不能是O(3)、O(12)等其餘任何數字,這是初學者經常犯的錯誤。
對於分支結構而言,不管是真,仍是假,執行的次數都是恆定的,不會隨着n的變大而發生變化,因此單純的分支結構(不包含在循環結構中),其時間複雜度也是O(1)。
線性階的循環結構會複雜不少。要肯定某個算法的階次,咱們經常須要肯定某個特定語句或某個語句集運行的次數。所以,咱們要分析算法的複雜度,關鍵就是要分析循環結構的運行狀況。
下面這段代碼,它的循環的時間複雜度爲O(n),由於循環體中的代碼須要執行n次。
下面的這段代碼,時間複雜度又是多少呢? 因爲每次count乘以2之後,就距離n更近了一分。也就是說,有多少個2相乘後大於n,則會退出循環。由2^x=n獲得x=log2n。因此這個循環的時間複雜度爲O(logn)。
下面例子是一個循環嵌套,它的內循環剛纔咱們已經分析過,時間複雜度爲O(n)。 而對於外層的循環,不過是內部這個時間複雜度爲O(n)的語句,再循環n次。因此這段代碼的時間複雜度爲O(n^2)。
若是外循環的循環次數改成了m,時間複雜度就變爲O(m*n)。 因此咱們能夠總結得出,循環的時間複雜度等於循環體的複雜度乘以該循環運行的次數。
那麼下面這個循環嵌套,它的時間複雜度是多少呢? 因爲當i=0時,內循環執行了n次,當i=1時,執行了n-1次,...當i=n-1時,執行了1次。因此總的執行次數爲:
用咱們推導大O階的方法,第一條,沒有加法常數不予考慮;第二條,只保留最高階項,所以保留n^2/2;第三條,去除這個項相乘的常數,也就是去除1/2,最終這段代碼的時間複雜度爲O(n^2)。
從這個例子,咱們也能夠獲得一個經驗,其實理解大O推導不算難,難的是對數列的一些相關運算,這更多的是考察你的數學知識和能力,因此想考研的朋友,要想在求算法時間複雜度這裏不失分,可能須要強化你的數學,特別是數列方面的知識和解題能力。
咱們繼續看例子,對於方法調用的時間複雜度又如何分析。 上面這段代碼調用一個函數function。
函數體是打印這個參數。其實這很好理解,function函數的時間複雜度是O(1)。因此總體的時間複雜度爲O(n)。
假如function是下面這樣的: 事實上,這和剛纔舉的例子是同樣的,只不過把嵌套內循環放到了函數中,因此最終的時間複雜度爲O(n^2)。
下面這段相對複雜的語句: 它的執行次數
根據推導大O階的方法,最終這段代碼的時間複雜度也是O(n^2)。
常見的時間複雜度如表2-10-1所示。 經常使用的時間複雜度所耗費的時間從小到大依次是:
]
咱們前面已經談到了O(1)常數階、O(logn)對數階、O(n)線性階、O(n^2)平方階等,至於O(nlogn)咱們將會在從此的課程中介紹,而像O(n^3),過大的n都會使得結果變得不現實。一樣指數階O(2^n)和階乘O(n!)等除非是很小的n值,不然哪怕n只是100,都是噩夢般的運行時間。因此這種不切實際的算法時間複雜度,通常咱們都不去討論它。
你早晨上班出門後忽然想起來,手機忘記帶了,這年頭,鑰匙、錢包、手機三大件,出門哪樣也不能少呀。因而回家找。打開門一看,手機就在門口玄關的臺子上,原來是出門穿鞋時忘記拿了。這固然是比較好,基本沒花什麼時間尋找。可若是不是放在那裏,你就得進去處處找,找完客廳找臥室、找完臥室找廚房、找完廚房找衛生間,就是找不到,時間一分一秒的過去,你忽然想起來,能夠用家裏座機打一下手機,聽着手機鈴聲來找呀,真是笨。終於找到了,在牀上枕頭下面。你再去上班,遲到。見鬼,這一年的全勤獎,就由於找手機給黃了。
找東西有運氣好的時候,也有怎麼也找不到的狀況。但在現實中,一般咱們碰到的絕大多數既不是最好的也不是最壞的,因此算下來是平均狀況居多。
算法的分析也是相似,咱們查找一個有n個隨機數字數組中的某個數字,最好的狀況是第一個數字就是,那麼算法的時間複雜度爲O(1),但也有可能這個數字就在最後一個位置上待着,那麼算法的時間複雜度就是O(n),這是最壞的一種狀況了。
最壞狀況運行時間是一種保證,那就是運行時間將不會再壞了。在應用中,這是一種最重要的需求,一般,除非特別指定,咱們提到的運行時間都是最壞狀況的運行時間。
而平均運行時間也就是從機率的角度看,這個數字在每個位置的可能性是相同的,因此平均的查找時間爲n/2次後發現這個目標元素。
平均運行時間是全部狀況中最有意義的,由於它是指望的運行時間。也就是說,咱們運行一段程序代碼時,是但願看到平均運行時間的。可現實中,平均運行時間很難經過分析獲得,通常都是經過運行必定數量的實驗數據後估算出來的。
對算法的分析,一種方法是計算全部狀況的平均值,這種時間複雜度的計算方法稱爲平均時間複雜度。另外一種方法是計算最壞狀況下的時間複雜度,這種方法稱爲最壞時間複雜度。通常在沒有特殊說明的狀況下,都是指最壞時間複雜度。
咱們在寫代碼時,徹底能夠用空間來換取時間,好比說,要判斷某某年是否是閏年,你可能會花一點心思寫了一個算法,並且因爲是一個算法,也就意味着,每次給一個年份,都是要經過計算獲得是不是閏年的結果。還有另外一個辦法就是,事先創建一個有2050個元素的數組(年數略比現實多一點),而後把全部的年份按下標的數字對應,若是是閏年,此數組項的值就是1,若是不是值爲0.這樣,所謂的判斷某一年是不是閏年,就變成了查找這個數組的某一項的值是多少的問題。此時,咱們的運算是最小化了,可是硬盤上或內存中須要存儲着2050個0和1。
這是經過一筆空間上的開銷來換取計算時間的小技巧。到底哪個好,其實要看你用在什麼地方。
算法的空間複雜度經過計算算法所需的存儲空間實現,算法空間複雜度的計算公式記爲:S(n)=O(f(n)),其中,n爲問題的規模,f(n)爲語句關於n所佔存儲空間的函數。