1、問題、問題實例、算法的概念區分。
一個例子說明一下:
問題:判斷一個正整數N是否為素數 #問題是需要解決的一個需求
問題實例:判斷1314是否為素數? #問題實例是該問題的一個具體例子
算法:解決這個問題的一個計算過程描述。 #算法是對計算過程的嚴格描述
2、算法的性質。
有窮性、能行性、確定性、終止性、輸入/輸出。
3、算法的描述
自然語言(容易歧義)
自然語言+數學公式(簡單方便,還是歧義)
嚴格形式描述(比如圖靈機模型,非常麻煩,難以閱讀)
類似編程語言描述
偽代碼的形式
該書主要是采用后兩者的描述方式進行的。
4、算法與程序的區別
對算法的描述中,有一種就是用編程語言來進行描述。基於這種認識,程序可以看做是采用編程語言進行描述算法的一種實現。但是程序自己的特點又跟具體編程語言的實現有關系。因此,一般我們把抽象描述一個計算過程稱之為算法。而把一個計算在某種語言的實現稱之為程序。
5、算法設計與分析
介紹了6種算法設計模式
枚舉法(枚舉全部、找出最優解)、貪心法(根據已有信息,先部分求解,再基於部分得到完整的解)、分治法(將一個復雜問題化解為很多簡單的子問題,對這些子問題分別求解,並組合起來得到復雜問題的解)、動態規划法(對於一些復雜的問題,不能一下求解出來。在求解步驟中,不斷積累已知信息,然后動態選擇已知的最好求解路徑)、回溯法(通過探索方式求解,當選擇一個方向探索時發現無解,就回溯到前面探索的那個路口往其他方向繼續探索,直到得出解)、分支界限法(回溯法的改良版本,它是在探索的過程中,根據已知的信息如果發現這個選擇是錯誤的,就及早將其刪除,用來縮小求解空間,加速問題求解的過程)。
當然,這些算法模式是可以混合使用的。
在計算過程中算法是會不斷消耗資源的,包括空間資源跟時間資源。而弄清楚算法耗費資源的多少就是算法分析的主要任務。
6、算法的代價及其度量
當一個算法消耗資源時,那么怎么來度量消耗的資源就成為了一個問題。因為計算的代價通常與實例的規模有關系(比如計算1013是否為素數與計算1001313130113是否為素數所花費的時間通常是不同的),因此,人們提出一個方法就是把一個計算開銷定義為問題規模的函數。
也就是說,算法分析就是針對一個具體的算法,來確定一個函數關系。這個函數以問題實例的規模n為參量,來反映出這個算法在處理規模n的問題時所消耗的時間(或者空間)代價。
還有一些問題。
1>對於同一個問題,如果它的衡量標准不統一的話,那么得到的算法代價就會差距很大
例如計算素數問題,按照整數的數值作為標准和按照數字串長度作為標准是完全不一樣的。這里,整數的數值與其數字串長度有着指數的關系。因此肯定這兩個不同標准下得到的算法分析將也會差異很大。
2>對於同一個問題,即使對於同樣規模的實例,計算的代價也可能不同
例如同樣100個十進制整數,如果為偶數的話,很快就能得出結果。如果為奇數,則要花費很長的時間。
針對這些情況,我們在度量算法的時候。存在幾種考慮:
算法A完成工作最少需要多長時間? #這個沒有意義,因為它沒有代表價值。
算法A完成工作最長需要多長時間? #在實際中,最主要是關注這種情況。
算法A完成工作平均需要多長時間? #對算法A的一個全面評價,但是沒有保證。
由於這些問題,對於一個算法分析,給出一個具體精確的描述通常是非常困難的。因此退而求其次,我們設法估算算法復雜性的量級。這樣,對於算法的時間和空間性質,最重要的是其量級和趨勢,這些是算法代價中的主要部分,而常量因子可以忽略不計了。
基於這些考慮,提出”大O記法“來描述算法的性質。
其中最常用是下面的一組漸進復雜度函數:
O(1)、O(logn)、O(n)、O(nlogn)、O(n²)、O(n³)、O(2n)
從圖中就可以看出,隨着實例規模的增大,它的算法復雜度增長趨勢明顯差異很大。算法的復雜度越高,其實施的代價隨着規模增大而增大長的速度就快。
當然,這種算法復雜度的分析是具有實際的意義的。
比如,預測天氣預報的程序,如果在今天晚上7點之前不能計算出預測明天的結果,那么這個算法就毫無意義。
解決同一問題的不同算法
例子:求斐波那契數列
1>遞歸算法
def fib(n):
if n < 2:
return1
return fib(n-1) + fib(n-2)
2>遞推算法
def fib(n):
f1, f2 = 1, 1
for i in range(1, n):
f1, f2 = f2, f1+f2
return f2
當使用遞歸算法的時候,它的時間代價是呈指數增長的。因此,當n比較大時候,這一計算就需要很長的一段時間。
當使用遞推算法的時候,它的時間代價是呈線性增長的。所以,相比較遞歸算法,這個有明顯的時間優勢。
7、算法分析
算法分析的目的是為了推導出算法的復雜度。書中給出了一些簡單的規則
1> 基本操作
其時間復雜度為O(1)。如果是函數調用,應該將其時間復雜度帶入,參與整體時間復雜度的計算。
2> 加法規則(順序復合)
如果算法時兩個部分的順序復合,其復雜度是這兩個部分的復雜度之和。
T(n) = T1(n) + T2(n) = O(T1(n)) + O(T2(n)) = O(max(T1(n), T2(n)))
由於忽略了常量因子,加法等於求最大值,所以取T1(n) 和 T2(n)中復雜度較高的一個。
3> 乘法規則(循環結構)
如果算法是一個循環,循環體將執行T1(n)次,每次執行需要T2(n)時間,那么:
T(n) = T1(n) * T2(n) = O(T1(n)) * O(T2(n)) = O(T1(n) * T2(n))
4> 取最大規則(分支結構)
如果算法時條件分支,兩個分支的時間復雜性分別為T1(n)和T2(n),那么:
T(n) = O(max(T1(n), T2(n)))
利用一個例子計算一下:
for i in range(n):
for j in range(n):
x = 0.0 #O(1)
for k in range(n):
x = x + m1[i][k] * m2[k][j] #O(1)
m[i][j] = x #O(1)
x = x + m1[i][k] * m2[k][j] 這個操作是基本操作O(1)。
在for k in range(n)這個循環體內,它要執行n次。因此,這里就是O(1) * n = O(n)
在for j in range(n)這個循環體內,執行一次需要的時間為O(1) * n + O(1) + O(1) = O(max(O(n), O(1), O(1))) = O(n),而它也需要執行n次。因此這里就是O(n) * n = O(n²)
在for i in range(n)這個循環內,執行一次需要時間為O(n²),它也需要執行n次。因此整個算法時間復雜度為O(n³)。
8、python的計算代價
時間開銷
python的很多基本操作並不是常量時間的。因此,需要我們在寫程序的時候要注意使用合適的數據結構。在書中列舉了很多,比如list的一般加入/刪除。還有dict操作的查詢和加入新的關鍵碼(平均是O(1),但是最壞的是O(n),主要是利用哈希表技術構造成的)
空間開銷
python關於空間開銷有兩個問題需要注意。
1> python的組合對象沒有設置最大的元素個數。當對一個表不斷添加元素,后來又刪除元素,讓表變小,但是占用的存儲空間並不會變少。
2> python有着自己的存儲管理系統,會將不用的對象進行回收(也就是python的垃圾回收機制)。
最后,還有一點需要注意。就是在考慮程序開發的時候,不但要選擇比較好的算法,還要考慮如何做出良好的實現。例如:
def test1(n):
lst = []
for i in range(n*10000):
lst = lst + [i]
return lst
def test4(n):
return lst(range(n*10000))
上面的兩種方式都能建立一個包含0-10000的整數,但是test1毫無必要的構造了很多的復雜結構,這樣使整個算法的時間代價變得更高,從而損害了其可用性。這種情況就是高級語言程序中存在的一些“效率陷阱”。