我們經常會說乙個演算法快不快,這個可以由實驗得出,也可以通過分析複雜度得出。實驗需要大量不同的輸入才更全面準確,否則片面地看某個輸入下的表現,是比較偏頗的。分析複雜度(通常分析最壞,因為平均涉及輸入的概率分布,依靠假設或者實驗和經驗)有時候並不是乙個簡單的事,簡單的情況是遍歷 for(int i = 0; i != n; i++) 的這種情況,顯然是o(n)的複雜度。但是一些複雜的情況就比較難辦了,舉例來說:
a. 棧操作: 除了push,pop,新增乙個操作叫multipop。
multipop(s,k)
while not empty(s) && k != 0
do pop(s)
k <- k-1
假設棧的大小為s,那麼multipop的複雜度為min(s,k)。
那麼現在有乙個空棧,進行n次操作,三種操作各種可重排列都有可能。問這n次操作的複雜度是多少?
b. 二進位制計數器: 乙個k位的二進位制數a,不斷加1。這個二進位制數用k位陣列表示,每一位為0或者1。 加1的操作叫increment。這個操作leetcode上也正好有類似的題目
plus one,它是十進位制數。
increment(a)
i <- 0
while i < length[a] && a[i] == 1
do a[i] <- 0
i <- i+1
if i < length[a]
then a[i] <- 1
顯然最差情況末尾k-1個1的時候,複雜度為o(k)。
那麼,現在從0開始加到n。問複雜度是多少?
初看上去,第乙個問題multipop最差是當有n-1個元素時,之前push了n-1次,需要o(n),也就是一次操作最差o(n),n次操作最差o(n*n)。第二個類似地也是o(n*n)。但是這個複雜度估計得太寬鬆了,要求更精確一點的複雜度就不是一眼看出的了。這個時候,引入了平攤分析。
平攤分析被引入實際上是為了解決某操作被多次呼叫,不同情況下各次呼叫複雜度不同的問題。該操作如果分別考慮不同情況的各次來計算,過於複雜,而作為乙個整體的時候,考慮該操作各情況呼叫的平攤複雜度,計算起來會容易不少(或者說更容易描述)。用書上的話說,「平攤分析可以用來證明在一系列的操作中,通過對所有操作求平均之後,即使對其中單一的操作具有較大的代價,平均代價還是很小的。平攤分析與平均情況分析的不同情況在於它不牽涉到概率;平攤分析保證在最壞的情況下,每個操作具有平均效能。」後面一句就是表示,單次操作平攤代價累積起來,大於等於總的最壞情況,當然,從複雜度上盡可能地逼近最壞情況。
介紹了三種分析方法:1.聚集分析;2,記賬方法;3勢能方法。
1.聚集分析
所謂聚集分析,實際上就是從整體考慮,考慮多次操作總的複雜度。
比如問題a,對於乙個空棧,n次操作的複雜度為o(n)。因為物件被壓入後最多被彈出一次,則呼叫pop的次數(包括了multipop裡的pop)最多等於push的次數,最多為n。通常我們算總的複雜度其實就到此為止了,一定要算出平攤複雜度的話就除以n,三個棧操作的平攤複雜度都是o(1)。
問題b中,對a的操作,每次呼叫,a[0]都會翻轉,a[1]每兩次翻轉一次,a[2]每4次翻轉一次...如果進行n次操作,那麼他們分別有n,n/2,n/4...(下取整)次翻轉。等比數列,加起來為小於2n次。n次總複雜度為o(n),單次的平攤代價為o(1)。
2.記賬方法
書上原話是「我們對不同的操作賦予不同的費用,某些操作的費用比它們的實際代價或多或少。我們對乙個操作的收費的數量稱為平攤代價。」。其實,操作起來,就相當於我先假設單次某操作的代價,假如我估計某費時的瓶頸操作平攤複雜度為o(1),那我完全可以讓單次操作的代價為某乙個常數k(通常要略大於這個操作的實際代價),多的存下來,可以把存款放出去給其他操作。那麼,因為有的操作會存,有的操作會花,不同操作有不同的平攤代價,聚集分析中平攤代價是一樣的。要保持分析中代價一直是上界,存款不允許為負。
對問題a,令push代價為2。相當於1用來支付push,同時放乙個在上面,pop的時候拿走。那麼pop和multipop都為o(0)。總的為o(n)。
對問題b,把某一位設為1代價為2,存1用來支付把這一位設為0,那麼設為0代價為0。總的為o(n)。
3.勢能方法
勢能方法英文叫potential method,勢能還是翻譯得很好的,因為它是與資料結構相關而不是與動作相關。比如拿棧來說,棧裡有多少元素我們就認為它有多少勢能,這是一種可能的做法,這也正是這個結構的potential。平攤代價就由實際代價加上增加勢能變化量。cp(i) = c(i) + s(i) - s(i-1)。n個操作就是σcp(i) = σc(i) + s(n) - s(0)。要保持上界,就得讓s(n)-s(0)永遠非負。通常,讓s(0)等於0,那麼接下來s(n)>= 0就好。我覺得勢也是一種存款。因而平攤操作重點在於考慮勢的增加而不是減少(勢差為負可以用來減實際代價,使平攤代價變少,如下ab問題)
對問題a,把勢函式定義為棧裡元素個數。那麼push平攤就為1+1=2,pop1-1= 0,multipop同pop一樣也為0.所以o(n)。
對問題b,把勢函式定義為計數器中1的個數。第i次操作復位t(i),置位1,c(i)=t(i)+1,勢差為1-t(i),所以平攤代價為2.所以為o(n)。
其實,注意如果考慮s(n)-s(0)的複雜度,那麼就可以不要求s(n)-s(0)。因為σcp(i) = σc(i) + s(n) - s(0),現在知道了σcp(i)的複雜度,又知道 s(n) - s(0),顯然可以知道σc(i) 的複雜度。
最後,書裡用動態表的操作來為乙個例子再闡述了分析過程。這裡我用
kmp為例來闡述。論
kmp-compute-prefix過程。
kmp-compute-prefix(p)
m <- length[p]
π[1] <- 0
k <- 0 // just like q above, number of char matched
for q <- 1 to n // like i above, and actually it is state, so its name is q
while k>0 && p[k+1] != s[q] // p range from 1 to n, not 0 to n-1
do k <- π[k]
if p[k+1] == s[q] then k <- k+1
π[q] <- k
return π
首先,用記賬法。對π[q]賦值這個操作我們給它記2,1用於自己的開銷,存1到q位置上。if p[k+1] == s[q] then k <- k+1 這個開銷複雜度是等同於π[q] <- k,自然可以用π[q]賦值這個操作代表了。問題在於while迴圈,這個迴圈是乙個讓k退步的乙個操作,它不斷地取π[q],這個取的開銷可以用放在上面的存款支付了.由於π[q]總是指向乙個有存款的地方(一開始我們在0,1兩處各放上1,這是o(1)的)。所以總是有存款可花的,保證了上界。也就是while迴圈平攤為了0.總開銷為o(n)。
也可以用勢能法。也是書上的方法(在kmp一節)。k的勢就是當前狀態k。顯然開始為0,以後總大於等於0.顯然勢最多加1,因為狀態最多前進一步(也就是if語句)。後退(也就是while迴圈)是可以很多的,這些代價被負的勢差所抵消。while和if都是在操作勢,後面賦值的開銷是o(1),勢的平攤開銷(至多)是o(1).所以最後為o(n)。
對 kmp-match(s,p) 可以用類似的方法,不再贅述。
讀書筆記 演算法導論
第2章演算法入門 浮於表面不如深入其中,送給自己,自己是最大的敵人,那麼就盡最大努力去克服自己,沉思,冷靜,不浮躁!勘誤 在演算法導論第9頁,扼要的扼 內容提要 1 偽 的表示方法 2 插入排序演算法分析 3 迴圈不變式 4 演算法設計之分治法 divide and conquer 5 合併排序演算...
演算法導論 讀書筆記2010 12 6
演算法就是一系列的計算步驟,用來將輸入資料轉換為輸出結果。資料結構師儲存和組織資料的一種方式,以便於對資料進行訪問和修改。插入排序演算法,對n個資料項的時間大約是c1n 2,其中c1是乙個不依賴於n的常量。亦即該演算法所需的時間大致與n 2成正比。合併排序演算法,排序n個資料項的時間大約是c2log...
《演算法導論》讀書筆記(一)
理解 輸入到輸出的計算過程稱為演算法。1.演算法描述 2.證明演算法正確性 3.分析演算法效率 兩個例子 1.插入排序 思想 從未排序的序列中取出乙個元素,將其插入到已排序序列的正確位置。實現 include include using namespace std int main for int ...