**平攤分析**在计算机科学中，是用於算法分析中的方法，平攤分析常用於分析資料結構（動態的資料結構），在使用平攤分析前須知道資料結構各種操作所可能發生的時間，並計算出最壞情况下的操作情況並加以平均，得到操作的平均耗费时間。平摊分析只能確保最坏情况性能的每次操作耗费的平均时间，並不能確認平均情况性能。

一個簡單的例子，一個長度為n的list，在list的最後要加入一筆新的資料此時要花費的操作時間為O(n)，此時也是加入新的資料的最糟糕的情況。但是，一个 *n* 个插入的操作序列仍然可以在 O(*n*) 的时间内完成，因为剩下的插入可以在常数时间内完成，因此*n* 个插入可以在 O(*n*) 的时间内完成。因此每操作的平摊耗费为O(*n*) / *n* = O(1)。

注意平摊分析与平均时间分析和概率算法的概率分析不同。在平均时间分析中，我们平均化所有可能的输入；在概率算法的概率分析中，我们平均化所有可能的随机选择；在平摊分析中，我们平均化一系列操作的耗费。平摊分析假设的是最坏情况输入并且通常不运行随机选择。

平摊分析中几种常用的技术:

* 聚合分析决定 *n* 个操作序列的耗费上界*T*(*n*)，然后计算平均耗费为 *T*(*n*) / *n*。
* 记账方法确定每个操作的耗费，结合它的直接执行时间及它在对运行时中未来操作的影响。通常来说，许多短操作增量累加成「债」，而通过减少长操作的次数来「偿还」。
* 势能方法类似记账方法，但通过预先储蓄「势能」而在需要的时候释放。

平攤分析種類
------

### 聚集法(Aggregate method)

計算n個操作的時間複雜度上限T(n)
平攤T(n)至每一個操作，每一個操作的平攤成本是T(n)/n

### 記帳法(Accounting method)

執行花費較低的operations時先存credit未雨綢繆, 供未來花費較高的operations使用

對每個操作定義一個合法的平攤成本(amortized cost)
假設c\_i為第i個操作的actual cost，{\displaystyle {\hat {c\_{i}}}}為第i個操作的amortized cost

若{\displaystyle c\_{i}<{\hat {c\_{i}}}}，則credit={\displaystyle {\hat {c\_{i}}}-c\_{i}}，我們把credit存起來(deposited)，未來可以提取(withdraw)
若{\displaystyle c\_{i}>{\hat {c\_{i}}}}，則提取credit

設定每個操作的平攤成本(amortized cost)後，要做valid check確保credit不可以是0，也就是說
{\displaystyle \sum \_{k=1}^{n}{\hat {c\_{i}}}\geq \sum \_{k=1}^{n}c\_{i}}

### 位能法(Potential method)

定義一個位能函數(potential function){\displaystyle \Phi (D)}，將資料結構D(例如: 堆疊)的狀態對應到一個實數

{\displaystyle D\_{0}}: 資料結構D的初始狀態
{\displaystyle D\_{i}}: 資料結構D經過i個操作後的狀態
{\displaystyle c\_{i}}: 第i個操作的actual cost
{\displaystyle {\hat {c\_{i}}}}: 第i個操作的amortized cost
定義{\displaystyle {\hat {c\_{i}}}=c\_{i}+\Phi (D\_{i})-\Phi (D\_{i-1})}

{\displaystyle \sum \_{k=1}^{n}{\hat {c\_{i}}}=\sum \_{k=1}^{n}(c\_{i}+\Phi (D\_{i})-\Phi (D\_{i-1}))=(\sum \_{k=1}^{n}c\_{i})+\Phi (D\_{n})-\Phi (D\_{0})}

為了滿足{\displaystyle \sum \_{k=1}^{n}{\hat {c\_{i}}}\geq \sum \_{k=1}^{n}c\_{i}}

我們定義{\displaystyle \Phi (D\_{n})-\Phi (D\_{0})\geq 0} , 通常令{\displaystyle \Phi (D\_{0})=0} 和 {\displaystyle \Phi (D\_{n})\geq 0}

例子
--

### 堆疊(stack)的平攤分析

我們定義一個堆疊有下列操作

| 操作(operation) | 說明 | actual cost |
| --- | --- | --- |
| S.push(x) | 將一個元素x放入堆疊S中 | {\displaystyle O(1)} |
| S.pop() | 把堆疊S中最上面的元素取出 | {\displaystyle O(1)} |
| S.multi-pop(k) | 一次pop k個元素 | {\displaystyle O(\min(\left\vert S\right\vert ,k))} |

S.mult-pop(k)的程式碼如下

```
def multi\_pop(k):
	while (not S.empty()) and (k>0):
		S.pop()
		k -= 1

```

接下來我們分別使用聚集法(aggregate method), 記帳法(Accounting method), 位能法(Potential method)求出"堆疊一個操作的平攤成本是O(1)"

#### 使用聚集法(aggregate method)分析堆疊操作的平攤成本

令{\displaystyle n\_{push}}是S.push(x)的執行次數，{\displaystyle n\_{pop}}是S.pop()的執行次數，{\displaystyle n\_{multi-pop}}是S.multi-pop(k)的執行次數

{\displaystyle n=n\_{push}+n\_{pop}+n\_{multi-pop}}為總執行次數

| 操作(operation) | actual cost | 執行次數 |
| --- | --- | --- |
| S.push(x) | {\displaystyle O(1)} | {\displaystyle n\_{push}} |
| S.pop() | {\displaystyle O(1)} | {\displaystyle n\_{pop}} |
| S.multi-pop(k) | {\displaystyle O(\min(\left\vert S\right\vert ,k))} | {\displaystyle n\_{multi-pop}} |

因為一個堆疊S如果是空的，就不能執行pop了，也就是說可以pop或multi-pop的元素個數不會超過S中push進去的元素個數

所以{\displaystyle n\_{pop}+n\_{multi-pop}\leq n\_{push}}

假設{\displaystyle T(n)}是執行n個操作的時間複雜度上限

{\displaystyle T(n)=O(1)\times n\_{push}+O(1)\times n\_{push}=O(n)}
所以堆疊一個操作的平攤成本為{\displaystyle O(n)/n=O(1)}

#### 使用記帳法(Accouting method)分析堆疊操作的平攤成本

我們假設S.push(x), S.pop(), S.multi-pop(k)的amortized cost分別為2, 0, 0，如下表所示

| 操作(operation) | actual cost {\displaystyle c\_{i}} | amortized cost {\displaystyle {\hat {c\_{i}}}} |
| --- | --- | --- |
| S.push(x) | 1 | 2 |
| S.pop() | 1 | 0 |
| S.multi-pop(k) | {\displaystyle O(\min(\left\vert S\right\vert ,k))} | 0 |

| Valid Check 
證明: {\displaystyle \sum \_{k=1}^{n}{\hat {c\_{i}}}\geq \sum \_{k=1}^{n}c\_{i}} |
| --- |
| {\displaystyle {\color {Red}proof:}}
push進入堆疊S的元素會存入credit $1
pop(S), multi-pop(S, k) 會取出這些元素的credit $1 |

因此每個操作的平攤成本是O(1)

  

#### 使用位能法(potential method)分析堆疊操作的平攤成本

我們定義位能函數{\displaystyle \Phi (D)}為執行i個操作後，堆疊內的元素個數

{\displaystyle \Phi (D\_{0})=0} ，因為堆疊一開始是空的
{\displaystyle \Phi (D\_{i})\geq 0} ，因為堆疊的元素個數一定{\displaystyle \geq 0}
計算堆疊S每一個操作的平攤成本

| 操作(operation) | 平攤成本(amortized cost) {\displaystyle {\hat {c\_{i}}}} |
| --- | --- |
| S.push(x) | {\displaystyle {\hat {c\_{i}}}=c\_{i}+\Phi (D\_{i})-\Phi (D\_{i-1})=1+(\left\vert S\right\vert +1)-\left\vert S\right\vert =2} |
| S.pop() | {\displaystyle {\hat {c\_{i}}}=c\_{i}+\Phi (D\_{i})-\Phi (D\_{i-1})=1+(\left\vert S-1\right\vert )-\left\vert S\right\vert =0} |
| S.multi-pop(k) | {\displaystyle {\hat {c\_{i}}}=c\_{i}+\Phi (D\_{i})-\Phi (D\_{i-1})=k+(\left\vert S-k\right\vert )-\left\vert S\right\vert =k-k=0} |

總平攤成本 {\displaystyle \sum \_{k=1}^{n}{\hat {c\_{i}}}=2\times n\_{push}=O(n)}，所以堆疊單一個操作的平攤成本是{\displaystyle O(n)/n=O(1)}

### 动态数组

动态数组的平摊分析
考虑一个随元素个数增加而增长的动态数组，比如Java的ArrayList或者C++的std::vector。如果我们的数组大小从4开始，那么来向其中增加四个元素的时间就是一个常数。然而，若要将第五个元素加入其中，那么会花费更多时间，因为我们此时必须要创建一个两倍于当前数组大小的数组（8个元素），把老元素拷贝到新数组中，然后增加一个新元素。接下来的三次加入操作也同样会花费常数时间，然后在数组被填满后则又需要一轮新的加倍扩充。

一般地，如果我们考虑任意一个任意的*n*大小的数组并对其进行*n* + 1次加入操作。我们注意到，所有的加入操作都是常数时间的，除了最后一个，它会花费O(n)时间在大小加倍上。因为我们进行了*n* + 1次加入操作，我们可以将数组加倍的时间平摊到所有的加入操作上，因此得到加入操作的平均时间是{\displaystyle {\tfrac {nO(1)+O(n)}{n+1}}=O(1)}。它是一个常数。

### 队列

使用Ruby實現的佇列，一个先進先出資料結構: 

```
class Queue
 def initialize
 @input = []
 @output = []
 end

 def enqueue(element)
 @input << element
 end

 def dequeue
 if @output.empty?
 while @input.any?
 @output << @input.pop
 end
 end

 @output.pop
 end
end

```

佇列操作及特性參考佇列條目，enqeue及deqeue操作時間複雜度為常數，
否則，dequeue需要{\displaystyle O(n)}時間將所有元素從輸入數組添加到輸出數組中，其中*n*是輸入數組的當前長度。 從輸入複製*n*元素後，我們可以在輸出數組再次為空之前執行*n*出隊操作，每次操作都需要一個恆定的時間。 因此，我們可以僅在o(n)時間執行一系列*n*出列操作，這意味著每個出列操作的攤銷時間是O(1) 。

或者，我們可以收取將任何項目從輸入數組複製到輸出數組的成本，以及該項目的早期排隊操作。 該計費方案將入隊的攤還時間加倍，但將出列的攤還時間減少到O(1)。

通常用法
----

* 在常见场合，我们把能很好平摊分析的算法称为"平摊算法"。
* 在线算法通常使用平摊分析。

参考资料
----

1. 1 2 3 4 5  Kozen, Dexter.. Cornell University. Spring 2011  [14 March 2015]. （原始内容存档于2018-10-03）.
2. ↑  Grossman, Dan.  (PDF). cs.washington.edu.  [2015年3月14日]. （原始内容存档 (PDF)于2015年4月2日）.
3. ↑ . MIT.  [2018-10-21]. （原始内容存档于2018-11-25）.