第二章 算法
????????算法是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,并且每條指令表示一個火多個操作。
2.1 開場白
????????開始增加難度
2.2 數據結構與算法的關系
????????數據結構中很多時候會講算法。
????????在數據結構中講到算法,是為何幫助理解好數據結構,并不會詳細談及算法的方方面面。
2.3 兩種算法的比較
????????逐個累計算法
????????高斯計算1+2+...100的算法
2.4 算法定義
????????算法是描述解決問題的方法。
????????算法(Algorithm)這個單詞最早出現在波斯數學家阿勒.花剌子蜜在公園825年(相當于我國的唐朝時期)所寫的《印度數字算術》中。如今普遍認可的對算法的定義是:
????????算法是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,并且每條指令表示一個或多個操作。
????????沒有通用的算法,就如同沒有包治百病的藥一樣。
????????現實世界中的問題千奇百怪,算法也要千變萬化,沒有一個通用的算法可以解決所有的問題。甚至解決一個小問題,很優秀的算法卻不一定適合。
????????為了解決某個或某類問題,需要把指令表示成一定的操作序列,操作序列包括一組操作,每一個操作都完成特定的功能,這就是算法了。
2.5 算法的特性
????????算法的五個基本特性:輸入、輸出、有窮性,確定性和可行性。
2.5.1 輸入輸出
????????算法具有零個或多個輸入。算法至少有一個或多個輸出。
2.5.2 有窮性
????????有窮性:指算法在執行有限的步驟之后,自動結束而不會出現無限循環,并且每一個步驟在可以接受的時間內完成。
2.5.3 確定性
????????確定性:算法的每一步都具有確定的含義,不會出現二義性。
????????相同的輸入只有唯一的輸出結果。算法的每個步驟都被精確定義而無歧義。
2.5.4 可行性
????????可行性:算法的每一步都必須是可行的,也就是說,每一步都能夠通過執行有限次數完成。
2.6 算法設計的要求
????????算法不是唯一的。同一個問題,可以有多重解決問題的算法。
2.6.1 正確性
????????正確性:算法的中卻行是只算法至少應該具有輸入,輸出和加工處理無歧義性,能正確反映問題的需求,能夠得到問題的正確答案。
????????算法的“正確”通常在用法上有很大的差別,大體分為以下四個層次。
????????1.算法程序沒有語法錯誤。
????????2.算法程序對于合法的輸入數據能夠產生滿足要求的輸出結果。
????????3.算法程序對于非法的輸入數據能夠得出滿足規格說明的結果。
????????4.算法程序對于靜心選擇的,深圳刁難的測試數據都有滿足要求的輸出結果。
2.6.2 可讀性
????????可讀性:算法設計的另一個目的是為了便于閱讀,理解和交流。
????????可讀性高有助于人們理解算法,晦澀難懂的算法往往隱含錯誤,不易被發現,并且難于調試和修改。
2.6.3 健壯性
????????健壯性,當輸入數據不合法時,算法也能做出相關處理,而不是產生異常或莫名其妙的結果。
2.6.4 時間效率高和存儲量低
????????最后,好的算法還應該具備時間效率高和存儲量低的特點。
????????時間效率值的是算法的執行時間,對于同一個問題,如果有多個算法能夠解決,執行時間段的算法效率高,執行時間長的效率低。
????????存儲量需求指的是算法在執行過程中需要的最大存儲空間,朱啊喲是算法程序運行時所占用的內存或外部硬盤存儲空間。
????????設計算法應該盡量滿足時間效率高和存儲量低的需求。
????????總結:好的算法應該就別正確性,可讀性,健壯性,高效率和低存儲量的特點。
2.7 算法效率的度量方法
2.7.1 事后統計方法
????????事后統計方法:這種方法主要是通過設計好的測試程序和數據,利用計算機計時器對不同算法編制的程序的運行時間進行比較,從而確定算法效率的高低。
????????這種方法的缺陷:
????????必須依據算法事前編制好程序,這通常需要花費大量的時間和精力。如果編制出來發現他根本是很糟糕的算法,不是竹籃打水一場空嗎?
????????時間的比較依賴計算機硬件和軟件等環境因素,有事會掩蓋算法本身的優劣。
????????算法的測試數據設計困難,并且程序的運行時間往往還與測試數據的規模有很大關系,效率高的算法在小測測試數據面前往往得不到體現。比如10個數字的排序,們不管用干什么算法,差異幾乎是0.而如果一百萬個隨機數字排序,那不同的算法差異就非常大了。
????????基于事后統計方法有這樣那樣的缺陷,我們考慮不予采納。
2.7.2 事前分析估算方法
????????事前分析估算方法:在計算機程序編制前,一句統計方法對算法進行估算。
????????高級語言編寫的程序在九三級上運行時鎖小孩的時間取決于下列因素:
????????1.算法采用的策略、方法
????????2.編譯產生的代碼質量
????????3.問題的輸入規模
????????4.機器執行指令的速度。
????????最終,在分析程序的運行時間時,最重要的是吧程序看成獨立于程序設計語言的算法和一系列步驟。
????????f(n) = n
????????f(n) = 1
????????f(n) = n^2
????????隨著n值的越來越大,它們在時間效率上的差異也就越來越大。
2.8 函數的漸進增長
????????輸入規模n在沒有限制的情況下,只要超過一個數值N,這個函數就總大于另一個函數,我們稱函數是漸進增長的。
????????函數的漸進增長:給定兩個函數f(n)和g(n),如果存在一個證書N,是的對于所有的n>N,f(n)總是比g(n)大,那么,我們說f(n)的增長漸進快于g(n).
????????因為隨著n的增大,加法常數其實是不影響做種的算法變化的,所以我們可以忽略加法常數。
????????與最高次項相乘的常數也并不重要。
????????判斷一個算法的效率時,函數中的常數和其他次要項常常可以忽略,而更應該關注主項(最高階項)的階數。
????????某個算法,隨著n的增大,它會越來越優于另一算法,或者越來越差于另一算法。這其實就是事前估算方法的理論依據,通過算法時間復雜度來估算算法時間效率。
2.9 算法時間復雜度
2.9.1 算法時間復雜度定義
????????在進行算法分析時,語句總的執行次數T(n)是關于問題規模n的函數進而分析T(n)隨n的變化情況并確定T(n)的數量級。算法的時間復雜度,也就是算法的時間量度,記作:T(n) = O(f(n))。它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸近時間復雜度,簡稱為時間復雜度。其中f(n)是sentiment規模n的某個函數。
????????用大寫O()來體現算法時間復雜度的記法,我們稱之為大O記法。
????????一般情況下,隨著n的增大,T(n)增長最慢的算法為最優算法。
????????顯然,由此算法時間復雜度的定義可知,我們的三個求和算法的時間復雜度分別為O(n),O(1),O(n^2)。我們分別給他們取了一個非官方的名稱,O(1)叫常數階,O(n)叫線性階,O(n^2)叫平方階,當然,還有其他的一些階,我們之后會介紹。
2.9.2 推導大O階的方法
????????1.用常數1取代運行時間中所有的加法常數
????????2.在修改后的雨欣次數函數中,值保留最高階項。
????????3.如果最高階項存在不是1,則去除與這個項相乘的常數。
????????得到的結果就是大O階。
????????似乎很簡單,但還要看看例子。
2.9.3 常數階
????????下面的這個算法,也就是剛才的第二種算法(高斯算法),為什么時間復雜度不是O(3)而是O(1)。
????????int sum = 0, n = 100;? /* 執行一次 */
????????sum = (1+n)*n/2;? ? ? /* 執行一次 */
????????printf("%d", sum);? ? /* 執行一次 */
????????這個算法的運行次數函數是f(n)=3。根究我們推導大O階的方法,第一步就是把常數項3改為1.在保留最高階項時發現,他根本沒有最高階項,所以這個算法的時間復雜度為O(1)。
????????無論n為多少,與問題的大小無關,執行時間恒定的算法,我們稱之為具有O(1)的時間復雜度,又叫常數階。
????????注意:不管這個常數是多少,我們都記作O(1),而不能是O(3),O(12)等其他任何數字,這是初學者常常犯的錯誤。
????????對于分支結構而言,無論是真,還是假,執行的次數都是恒定的,不會隨著n的變大而發生變化,所以單純的分支結構(不包含在循環結構中),其時間復雜度也是O(1)。
2.9.4 線性階
????????線性階的循環結構會復雜很多。要確定某個算法的階次,我們常常需要確定某個特定詞語或某個語句集運行的次數。因此我們要分析算法的復雜度,關鍵就是要分析循環結構的運行情況。
????????int i;
????????for(i = 0; i < n; i++)
????????{
? ? ????????/*時間復雜度為O(1)的程序步驟序列)*/
????????}
2.9.5 對數階
下面這段代碼的時間復雜度有事多少呢?
????????int count = 1;
????????while(count < n)
????????{
? ? ????????count = count * 2;
????????}
????????由于每次count乘以2之后,就距離n更近了一分。也就是說,有多少個2相乘后大于n,則會退出循環。由2x = n得到 x=log2n。所以這個循環的時間復雜度為O(logn)。
2.9.6 平方階
????????下面的例子是一個循環嵌套,它的內循環剛才我們已經分析過,時間復雜度為O(n)。
????????int i,j;
????????for(i = 0; i < n; i++)
????????{
? ? ????????for (j = 0; j < n; j++)
? ????????? {
? ? ? ????????? /* 時間復雜度為O(1) 的程序步驟序列 */
? ? ????????}
????????}
外層的循環,不過是內部這個時間復雜度為O(n)的語句,再循環n次。所以這頓啊代碼的時間復雜度為O(n^2)。
如果外循環的次數改為了m,時間復雜度就變為了O(mXn)。
????int i,j;
????for(i = 0; i < m; i++)
????{
? ? ? ? for (j = 0; j < n; j++)
? ????? {
? ? ? ? ????/* 時間復雜度為O(1)的程序步驟序列 */
? ????? }
????}
????????所以我們可以總結得出,循環的時間復雜度等于循環體的復雜度乘以改循環運行的次數。
? ? ? ? 那么下面這個循環嵌套,它的時間復雜度是多少呢?
????????int i,j;
????????for(i = 0; i <n; i++)
????????{
????????????for(j = i; j < n; j++)
????????????{
????????????????/*時間復雜度為O(1)的程序步驟序列*/
????????????}
????????}
????????用我們推導大O階的方法,第一條,沒有加法常數不予考慮,第二條只保留最高階項,因此保留 n^2/2;第三條,去除這個項相乘的常數,也就是去除1/2,最終這段代碼的時間復雜度為O(n^2)。
????????理解大O推導不難,難的是對數列的醫學相關運算,這更多的是考察你的數學知識能力,如果想考研,要想在求算法時間復雜度這里不失分,可能需要強化你的數學,特別是數列方面的知識和解題能力。
????????我們繼續看例子,對于方法調用的時間復雜度又如何分析?
????????int i,j;
????????for(i = 0; i < n; i++)
????????{
? ? ????????function(i);
????????}
????????void function(int count)
????????{
? ????????? print(count);
????????}
函數體是打印這個參數。其實這很好理解,function函數的時機復雜度是O(1)。所以整體的時間復雜度為O(n)。
假如function是下面扎樣的:
????????void function(int count)
????????{
????????? ? int j;
????????? ? for(j = count; j < n; j++)
????????? ? {
????????? ? ? ? /* 時間復雜度為O(1)的程序步驟序列 */
????????? ? }
????????}
事實上和剛才的例子是一樣的只不過把嵌套內循環放到函數中,所以最終的時間復雜度為O(n^2)
下面這段相對復雜的語句:
????????n++;
????????funciton(n);? ? /* 執行次數為n */
????????int i,j;
????????for(i = 0; i < n; i++)
????????{
????????? ? function(i);
????????}
????????for(i = 0; i < n; i++)
????????{
????????? ? for(j = i; j < n; j++)
????????? ? {
? ? ? ? ????????/* 時間復雜度為O(1)的程序步驟序列 */
????????? ? }
????????}
2.10常見的時間復雜度
常見的時間復雜度如下表
????????常用的時間復雜度所耗費的時間從小到大的依次是:
????????O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
????????我們前面已經談到了O(1)常數階,O(logn)對數階,O(n)線性階,O(n^2)平方階等,至于O(nlogn)我們將會在今后的課程中介紹,而像O(n^3),過大的n都會使得結果變得不現實。同樣指數階O(2^n)和階乘階O(n!)等除非是很小的n值,否則哪怕n只是100,都是噩夢般的運行時間。所以這種不切實際的算法時間復雜度,一般我們都不去討論它。
2.11 最壞情況與平均情況
????????最壞情況運行時間是一種保證,那就是運行時間將不會再壞了。在應用中,這是一種最重要的需求,通常,除非特別指定,我們提到的運行時間都是最壞情況的裕興時間。
????????平均時間是所有情況中最有意義的,因為他是期望的運行時間。但現實,平均運行時間很難通過分析得到,一般都是通過運行一定數量的試驗數據后估算出來的。
????????一般在沒有特殊說明的情況下,都是指最壞時間復雜度。
2.12 算法空間復雜度
????????閏年計算,可以寫一個判斷閏年的算法,也可以寫一個所有年份的列表,記錄哪一年是閏年。
????????算法的空間復雜度通過計算算法所需的存儲空間實現,算法控件復雜度的計算公式記作:S(n) = O(f(n)),其中,n為問題的規模,f(n)為語句相關于n所占存儲空間的函數。
????????一般情況下,一個程序在機器上執行時,除了需要存儲程序本身的指令,常數,變量和輸入數據外,還需要存儲對數據操作的存儲單元。若輸入數據所占空間只取決于問題本身,和算法無關,這樣只需要分析該算法在實現時所需的輔助單元即可。若算法執行時所需的輔助空間相對于輸入數據而言是個常數,則稱此算法為原地工作,空間復雜度為O(1)。
????????通常,我們都使用“時間復雜度”來指運行時間的需求,使用“空間復雜度”指空間需求。當不用限定詞地使用“復雜度”時,通常斗志時間復雜度。顯然我們本書的重點要講的還是算法的時間復雜度問題。
2.13 總結回顧
????????算法的定義:算法是解決特定問題求解步驟的描述,在計算機中為指令的有限序列,并且每條指令表示一個或多個操作。
????????算法的特性:有窮性,確定性,可行性,輸入,輸出。
????????算法的設計要求:正確性,可讀性,健壯性,高效率和低存儲量需求。
????????算法特性與算法設計容易混需要對比記憶。
????????算法的度量方法:事后統計(不科學,不準確),事前分析估算方法。
????????事前分析估算方法之前,我們先給出了函數漸進增長的定義。
????????函數的漸近增長:給定兩個函數f(n)和g(n),如果存在一個整數N,使得對于所有的n>N,f(n)總是比g(n)大,那么我們說f(n)的增長漸近快于g(n)。于是我們可以得出一個結論,判斷一個算法好不好,我們止痛膏少量的數據是不能做出準確判斷的,如果我們可以對比算法的關鍵執行次數函數漸近增長性,基本就可以分析出:某個算法,隨著n的變大,它會越來越優于另一個算法,或者越來越差于另一算法。
????????然后給出了算法時間復雜度的定義和推導大O階 的步驟。
推導大O階:
????????用常數1取代運行時間中的所有加法常數
????????在修改后的運行次數函數中,只保留最高階項。
????????如果最高階項存在且不是1,則去除與這個項相乘的常數。
????????得到的結果就是大O階。
????????推導很容易,但如何得到運行次數的表達式卻是需要數學功底的。
????????常見時間復雜度所耗時間的大小排列:
????????O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(2^n)<O(n!)<O(n^n)
????????給出類關于算法最壞情況和平均情況的概念,以及空間復雜度的概念。
2.14 結尾語
????????學計算機專業,做了很長時間的工作,終于明白了算法時間復雜度的估算。可以通過優化讓計算機更快更高效的。
????????好好利用算法分析工具,改進代碼,讓計算機輕松一點,自己也可以更加勝人一籌。