算法绪论
目錄
- 申明
- 1. 數據結構與算法關系
- 2. 兩種算法的比較
- 3. 算法定義
- 4. 算法的特性
- 4.1 輸入輸出
- 4.2 有窮性
- 4.3 確定性
- 4.4 可行性
- 5. 算法的設計要求
- 5.1 正確性
- 5.2 可讀性
- 5.3 健壯性
- 5.4 時間效率高和存儲量低
- 6. 算法效率的度量方法
- 6.1 事后統計法
- 6.2 事前分析估算法
- 7. 函數的逐漸增長
- 8. 算法時間復雜度
- 8.1 算法時間復雜度定義
- 8.2 推導大O階方法
- 8.3 常數階
- 8.4 線性階
- 8.5 對數階
- 8.6 平方階
- 9. 常見的時間復雜度
- 10. 最壞情況與平均情況
- 11. 算法空間復雜度
- 12. 總結回顧
- 13. 專欄知識鏈接
申明
??本文章屬于原創,其中參考了程杰老師《大話數據結構》行文結構和內容,侵刪。
1. 數據結構與算法關系
??數據結構和算法實際上是相輔相成的關系,只談數據結構的話,其實可以在很短的時間內介紹完,但是我們會不知道這些數據結構到底有何用處。在本專題談到的算法,是為了更好的理解數據結構,并不會講到算法的方方面面。
2. 兩種算法的比較
??如果讓你寫一個從1加到100度求和程序,你應該如何實現?下面這段代碼可能是大多數人的思路:
int i, sum = 0, n = 100; for (i = 1; i <= n; ++i) {sum = sum + i; } printf("sum result %d", sum);??然而這樣實現是否真的高效呢?其實用高斯公式可以這樣實現:
int sum = 0, n = 100; sum = (1 + 100) * n / 2; printf("sum result %d", sum);??可能你在計算機上運行了上面兩種程序,發現并沒有區別,但是如果將求和的范圍擴大到一千、一億呢?
3. 算法定義
??如今普遍認可的算法定義是:算法是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,并且每條指令表示一個或多個操作。
4. 算法的特性
4.1 輸入輸出
??**算法具有零個或多個輸入,至少有一個或者多個輸出。**輸出的形式可以是打印輸出,也可以是返回一個或多個值等。
4.2 有窮性
??有窮性:指算法在執行有限的步驟之后,自動結束而不會出現無限循環,并且每一個步驟在可以接受的時間內完成。
4.3 確定性
??確定性:算法的每一步驟都具有確定的含義,不會出現二義性。
4.4 可行性
??可行性:算法的每一步都必須是可行的,也就是說,每一步都能夠通過執行有限次數完成。
5. 算法的設計要求
5.1 正確性
??正確性:算法的正確性是指算法至少應該具有輸入、輸出和加工處理無歧義性、能正確反映問題的需求、能夠得到問題的正確答案。
??但是算法的“正確”通常在用法上有很大的差別,大體分為以下四個層次。
??1. 算法程序沒有語法錯誤。
??2.算法程序對于合法的輸入數據能夠產生滿足要求的輸出結果。
??3.算法程序對于非法的輸入數據能夠得出滿足規格說明的結果。
??4.算法程序對于精心選擇的,甚至刁難的測試數據都有滿足要求的輸出結果。
??層次4幾乎不能用逐一驗證的方法來驗證所有的輸入都能得到正確的結果,因此算法的正確性大部分情況下都不可能用程序來證明,而是用數學方法來證明。證明一個復雜算法在所有層次上都是正確的,代價非常昂貴。所以一般情況下,我們把層次3作為一個算法是否正確的標準。
5.2 可讀性
??可讀性:算法設計的另一個目的是為了便于閱讀、理解和交流。
5.3 健壯性
??一個好的算法應該能對輸入數據不合法的情況做合適的處理。比如輸入的時間或者距離不應該是負數等。
??健壯性:當輸入數據不合法時,算法也能做出相關處理,而不是產生異常或者莫名其妙的結果。
5.4 時間效率高和存儲量低
??時間效率指的是算法的執行時間,對于同一問題,如果有多個算法能夠解決,執行時間短的算法效率高,執行時間長的效率低。存儲量需求指的是算法在執行過程中需要的最大存儲空間,主要指算法程序運行時所占用的內存或者外部硬盤存儲空間。設計算法應該盡量滿足時間效率高和存儲量低的需求。
6. 算法效率的度量方法
6.1 事后統計法
??事后統計法:這種方法主要是通過設計好的測試程序和數據,利用計算機計時器對不同算法編制的程序等運行時間進行比較,從而確定算法效率的高低。
??但是這種方法顯然有很大的缺陷:它受計算機軟硬件等環境因素的影響,同時測試數據設計困難,并且程序的運行時間還與測試數據的規模有很大的關系,效率高的算法在小的測試數據面前往往得不到體現。
??所以基于時候統計法有這樣那樣的缺陷,我們考慮不予采納。
6.2 事前分析估算法
??事前分析估算法:在計算機程序編制前,依據統計方法對算法進行估算。
??一個用高級程序語言編寫的程序在計算機上運行時所消耗的時間取決于下列因素:
??1.算法采用的策略、方法。
??2.編譯產生的代碼質量。
??3.問題的輸入規模。
??4.機器執行指令的速度。
??第1條當然是算法好壞的根本,第2條需要由軟件來支持,第4條要看硬件性能。也就是說,拋開這些與計算機硬件、軟件有關的因素,一個程序的運行時間,依賴于算法的好壞和問題的輸入規模。所謂問題的輸入規模是指輸入量的多少。
??還是來看兩種求和算法的例子:
??第一種算法:
??第二種算法:
int sum = 0, n = 100; /* 執行1次 */ sum = (1 + 100) * n / 2; /* 執行1次 */ printf("sum result %d", sum); /* 執行1次 */??顯然,第一種算法執行了1+(n+1)+n+1次=2n+3次;第二種算法執行了1+1+1=3次。事實上兩種算法第一條和最后一條語句是一樣的,所以我們關注的代碼其實是中間那部分,我們把循環看作一個整體,忽略頭尾循環的判斷開銷,那么這兩個算法就是n次與1次的差距,好壞顯而易見。
??在分析程序的運行時間時,最重要的是把程序看成是獨立于程序設計語言的算法或一系列步驟。
7. 函數的逐漸增長
| n = 1 | 5 | 2 | 4 | 3 |
| n = 2 | 7 | 4 | 7 | 6 |
| n = 3 | 9 | 6 | 10 | 9 |
| n = 10 | 23 | 20 | 31 | 30 |
| n = 100 | 203 | 200 | 301 | 300 |
??通過觀察表格我們得出這樣的定義,輸入規模n在沒有限制的情況下,只要超過一個數值N,這個函數就總是大于另一個函數,我們稱函數是漸近增長的。
??函數的漸近增長:給定兩個函數f(n)和g(n),如果存在一個整數N,使得對于所有的n>N,f(n)總是比g(n)大,那么,我們說f(n)的增長漸近快于g(n)。
??從中我們發現,隨著n的增大,后面的+3還是+1其實是不影響最終的算法變化的,所以我們可以忽略這些加法常數。實際上通過上述表格舉一反三,最終判斷一個算法的效率時,函數中的常數和其他次要項常常可以忽略,而更應該關注主項(最高階項)的階數。
??**某個算法,隨著n的增大,它會越來越優于另一算法,或者越來越差于另一算法。**這其實就是事前估算法的理論依據,通過算法時間復雜度來估算算法時間效率。
8. 算法時間復雜度
8.1 算法時間復雜度定義
??在進行算法分析時,語句總的執行次數T(n)是關于問題規模n的函數,進而分析T(n)隨n的變化情況并確定T(n)的數量級。算法的時間復雜度,也就是算法的時間量度,記作:T(n)=O(f(n))。它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸近時間復雜度,簡稱為時間復雜度。其中f(n)是問題規模n的某個函數。
??這樣用大寫O()來體現算法時間復雜度的記法,我們稱之為大O記法。
??一般情況下,隨著n大增大,T(n)增長最慢的算法稱之為最優算法。我們通常把O(1)叫做常數階、O(n)叫做線性階、O(n2)叫平方階。
8.2 推導大O階方法
??推導大O階:
??1.用常數1取代運行時間中的所有加法常數。
??2.在修改后的運行次數函數中,只保留最高階項。
??3.如果最高階項存在且不是1,則去除與這個項相乘的常數。
??得到的結果就是大O階。
8.3 常數階
??如果不存在循環體,那么根據大O階大推導方法,不管這個常數是多少,我們都記作O(1),而不能是O(3),O(12)等其他任何數字,這是初學者常常犯的錯誤。
??對于分支結構而言,無論是真,還是假,執行的次數都是恒定的,不會隨著n的變大而發生變化,所以單純的分支結構(不包含在循環結構中),其時間復雜度也是O(1)。
8.4 線性階
??線性階的循環結構會復雜很多。要確定某個算法的階次,我們常常需要確定某個特定語句或者語句集運行的次數。因此,我們要分析算法的復雜度,關鍵就是要分析循環結構的運行情況。
??下面這段代碼,它的循環的時間復雜度為O(n),因為循環體中的代碼要執行n次。
8.5 對數階
int count = 1; while count < n) {count = count * 2;/* 時間復雜度為O(1)的程序步驟序列 */ }??由于每次count乘以2之后,就距離n更近了一分。也就是說,有多少個2相乘后大于n,則會退出循環。由2^n 得到x=log_2?n(以2為底n的對數)。所以這個循環的時間復雜度為O(logn)。
8.6 平方階
??下面這個例子是一個嵌套循環,外層循環次數改為了m,時間復雜度為O(m*n)。
int i,j; for(i = 0; i < m; i++) {for(j = 0; j < n; j++) { /* 時間復雜度為O(1)的程序步驟序列 */} }9. 常見的時間復雜度
??常見時間復雜度如下表:
??注意,經常將log2n(以2為底的對數)簡寫成logn
??常用的時間復雜度所耗費的時間從小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
10. 最壞情況與平均情況
??我們查找一個有n個隨機數字數組中的某個數字,最好的情況是第一個數字就是,那么算法的時間復雜度為O(1),但是也有可能這個數字在最后,那么算法的時間復雜度就是O(n),這是最壞的一種情況了。
??最壞情況運行時間是一種保證,那就是運行時間將不會再壞了。在應用中,這是一種最重要的需求,通常,除非特別指定,我們提到的運行時間都是最壞情況的運行時間。
??而平均運行時間也就是從概率的角度看,這個數字在每一個位置的可能性是相同的,所以平均的查找時間為n/2次后發現這個目標元素。
??**平均運行時間是所有情況中最有意義的,因為它是期望的運行時間。**可現實中,平均分析時間很難通過分析得到,一般都是通過運行一定數量的實驗數據后估算出來的。
??對算法的分析,一種方法是計算所有情況的平均值,這種時間復雜度的計算方法稱為平均時間復雜度。另一種方法是計算最壞情況下的時間復雜度,這種方法稱為最壞時間復雜度。一般在沒有特殊說明的情況下,都是指最壞時間復雜度。
11. 算法空間復雜度
??算法的空間復雜度通過計算算法所需的存儲空間實現,算法空間復雜度的計算公式記作:S(n) = O(f(n)),其中,n為問題的規模,f(n)為語句關于n所占存儲空間的函數。
??一般情況下,一個程序在機器上執行時,除了需要存儲程序本身的指令、常數、變量和輸入數據外,還需要存儲對數據操作的存儲單元。若輸入數據所占空間只取決于問題本身,和算法無關,這樣只需要分析該算法在實現時所需的輔助單元即可。若算法執行時所需的輔助空間相對于輸入數據量而言是個常數,則稱此算法為原地工作,空間復雜度為O(1)。
??通常,我們都使用“時間復雜度”來指運行時間的需求,使用“空間復雜度”指空間需求。當不用限定詞地使用“復雜度”時,通常都是指時間復雜度。
12. 總結回顧
??算法的定義:算法是解決特定問題求解步驟的描述,在計算機中為指令的有限序列,并且每條指令表示一個或多個操作。
??算法的特性:有窮性、確定性、可行性、輸入、輸出。
??算法的設計要求:正確性、可讀性、健壯性、高效率和低存儲量需求。
??算法特性和算法設計容易混,需要對比記憶。
??算法的度量方法:事后統計方法(不科學、不準確)、事前分析估算方法。
??推導大O階:
??1.用常數1取代運行時間中的所有加法常數。
??2.在修改后的運行次數函數中,只保留最高階項。
??3.如果最高階項存在且不是1,則去除與這個項相乘的常數。
??得到的結果就是大O階。
??常用的時間復雜度所耗費的時間從小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
13. 專欄知識鏈接
1. 數據結構緒論
2. 線性表詳解(靜態鏈表、單鏈表、雙向鏈表、循環鏈表)
總結