轉載自: http://codeplayer.org/2013/12/471
先來直觀的介紹一下什么是攤還分析:在攤還分析中,我們求數據結構的一個操作序列中所執行的所有操作的平均時間,來評價操作的代價。這樣,我們就可以說明一個操作的平均代價是很低的,即使序列中某個單一操作的代價很高。攤還分析不同於平均情況分析,它不涉及概率,它可以保證最壞情況下每個操作的平均性能。
在學習攤還分析的時候要注意,在攤還分析中賦予對象的費用僅僅是用來分析而已,不需要也不應該出現在程序中。通過做癱瘓分析,通常可以獲得對某種特定數據結構的認識,這種認識有助於優化設計。
聚合分析
使用聚合分析,我們可以證明對所有n,一個n個操作的序列最壞情況下花費的總時間為T(n)。因此,在最壞情況下,每個操作的平均時間,或攤還代價為T(n)/n。注意,此攤還代價是適用於每個操作的,即使序列中有多種類型操作。下面通過兩個例子來了解一下聚合分析。
棧操作
經典的棧操作,這里不過多敘述,它支持三種操作:
- PUSH(S, x),壓入對象x。
- POP(S),彈出一個對象。
- MULTIPOP(S, k),彈出k個對象,如果棧中對象的數量少於k,則將所有對象彈出。
我們來分析一下n個這三種操作在一個空棧上的運行時間。一個MULTIPOP操作的最壞情況時間為О(n),因為棧的大小為最大為n,PUSH和POP最壞情況時間均為1,假設序列中的有О(n)個MULTIPOP操作,所以我們通過分析每個操作的最壞情況時間得到操作序列的最壞情況時間為О(n^2),但是這不是一個確界。
通過使用聚合分析,考慮整個序列的n個操作,可以得到更好的上界。實際上雖然MULTIPOP操作的最壞情況時間很高,但是在一個空棧上執行PUSH、POP、MULTIPOP的操作序列,代價最多是О(n)。因為我們將一個對象壓入到棧后,最多只將其彈出一次,所以一個非空的棧,可以執行的POP次數最多為n。因此上述操作序列最多花費О(n)時間,而一個操作的平均時間為О(n) / n = О(1)。在聚合分析中,我們將每個操作的攤還代價設定為平均代價。所以,這三種操作的攤還代價均為О(1)。
這里我們並未使用概率分析就證明了一個棧操作的平均代價。我們實際上得出的是一個n個操作序列的最壞情況運行時間О(n),再除以n得到了每個操作的平均代價,或者說攤還代價。
二進制計數器遞增
一個k位二進制計數器遞增的問題的例子。簡單來說就是使用二進制來計數,並將這個二進制數放到一個數組中,可以使用一個INCREMENT的操作來對這個二進制數增加1。A.length = k,將最低位保存在A[0]中,最高位保存在A[k-1]中。INCREMENT的偽代碼如下:
1
2
3
4
5
6
|
i = 0
while i < A.length and A[i] == 1
A[i] = 0
i = i+1
if i < A.length
A[i] = 1
|
如下圖所示,在2~4行while循環時,我們希望將1加在第i位上。如果A[i]=1,那么加1操作會將第i位翻轉為0,並產生一個進位——在一次循環中將1加到i+1位上。否則循環結束,此時若i
與上一個例子類似,粗略的分析會的到一個正確但是不緊的界。最壞情況下翻轉數組上所有的位,INCREMENT執行一次花費Θ(k)時間,因此n個INCREMENT的最壞時間為О(nk)。但是通過攤還分析,我們可以得到最壞運行時間О(n),因為不可能每次INCREMENT都翻轉所有的位。實際上,對一個初值為0的計數器,執行n個INCREMENT的過車給你中,A[i]會翻轉n/(2^i)次。所以我們可以得到的執行翻轉操作的總數為:
所以可以得到n個INCREMENT操作序列的最壞情況時間為О(n),攤還代價為О(n)/n = О(1)。
核算法
用核算法進行攤還分析時,我們對不同操作賦予不同費用,賦予某些操作的費用可能多於或少於實際代價。我們將賦予一個操作的費用成為它的攤還代價。當一個操作的攤還代價超出其實際代價時,我們將差額存入數據結構中的特定對象,存入的差額成為信用。對於后續操作中攤還代價小於實際代價的情況,信用可以用來支付差額。
我們必須小心地選擇操作的攤還代價。如果我們希望通過分析攤還代價來證明每個操作的平均代價的最壞情況很小,就應確保操作序列的總攤還代價給出了序列總真實代價的上界。而且,這種關系必須對所有操作序列都成立。數據結構中存儲的信用恰好等於總攤還代價與總實際代價的差值。數據結構所關聯的信用必須一直非負值,如果在某個步驟,我們允許信用為負值,那么當時的總攤還代價就會低於總實際代價,對於到那個時刻為止的操作序列,總攤還代價就不再是總實際代價的上界了。
棧操作
為了說明攤還分析的核算法,再次使用棧的例子。我們賦予PUSH、POP、MULTIPOP如下的攤還代價:
PUSH 2
POP 0
MULTIPOP 0
PUSH時我們將1元的代價支付PUSH操作的實際代價,將剩余的1元存為信用,這1元實際上是作為將來被POP時代價的預付費。當執行一個POP時,並不繳納任何費用,而是使用存儲的信用來支付其實際代價,對於MULTIPOP也是一樣的。因為棧中元素的數量總是非負的,所以可以保證信用也非負的。因此,對任意n個PUSH、POP、MULTIPOP組成的序列,總攤還代價О(n)為總實際代價的上界。
二進制計數器遞增
在這個例子中,對一次置位操作,我們設其攤還代價為2元,用1元支付實際代價,1元存為信用,用來支付將來復位操作的代價。在任何時刻,計數器中任何為1的位都存有1元信用,這樣在復位的時候,我們就不需要支付任何費用了。
INCREMENT過程一次最多置位一次,因此攤還代價最多為2美元,計數器中1的個數永遠不會為負,所以對於n個INCREMENT操作的總攤還代價О(n)是總實際代價的上界。
勢能法
勢能法攤還分析並不預付代價表示為數據結構中特定對象的信用,而是表示為“勢能”,將勢能釋放即可用來支付未來操作的代價。我們將勢能於整個數據結構而不是特定對象相關聯。
工作方式如下。對一個初始數據結構D0執行n個操作,對每個i=1,2,...,n,令ci為第i個操作的實際代價,令Di為數據結構Di-1執行第i個操作得到的結果數據結構。勢函數Φ將每個數據結構Di映射到一個實數Φ(Di),此值即為關聯到數據結構Di的_勢_。第i個操作的攤還代價^ci用勢函數Φ定義為:
所以每個操作的攤還代價等於其實際代價加上此操作引起的勢能變化。則n個操作的總攤還代價為
不同的勢函數會產生不同的攤還代價,但攤還代價仍未實際代價的上界。在選擇勢函數時,我們常常發現可以做出一定的權衡,是否使用最佳勢函數依賴於對時間界的要求。
兩個例子還是盞和二進制計數器,不過勢能法分析起來使用公式計算較多,在這里公式寫起來不太方便就不詳細敘述了。總體思想就是將預支付的代價添加的整個數據結構的勢能中,將勢能釋放即可支付未來操作的代價。
勢能法具體的例子,雖然表面上看起來擴張與收縮會有О(n)的代價,但是實際上在表滿時將表擴張一倍、裝載因子為1/4時將表收縮一半的攤還代價僅為О(1)。