平摊分析

平攤分析（英語：Amortized analysis）在计算机科学中，是用於算法分析中的方法，平攤分析常用於分析資料結構（動態的資料結構），在使用平攤分析前須知道資料結構各種操作所可能發生的時間，並計算出最壞情况下的操作情況並加以平均，得到操作的平均耗费时間。平摊分析只能確保最坏情况性能的每次操作耗费的平均时间，並不能確認平均情况性能。 一個簡單的例子，一個長度為 n {\displaystyle n} 的list，在list的最後要加入一筆新的資料此時要花費的操作時間為 O ( n ) {\displaystyle O(n)} ，此時也是加入新的資料的最糟糕的情況。但是，一个 n {\displaystyle n} 个插入的操作序列仍然可以在 O ( n ) {\displaystyle O(n)} 的时间内完成，因为剩下的插入可以在常数时间内完成，因此 n {\displaystyle n} 个插入可以在 O ( n ) {\displaystyle O(n)} 的时间内完成。因此每操作的平摊耗费为 O ( n ) / n = O ( 1 ) {\displaystyle O(n)/n=O(1)} 。 注意平摊分析与平均时间分析和概率算法的概率分析不同。在平均时间分析中，我们平均化所有可能的输入；在概率算法的概率分析中，我们平均化所有可能的随机选择；在平摊分析中，我们平均化一系列操作的耗费。平摊分析假设的是最坏情况输入并且通常不运行随机选择。 平摊分析中几种常用的技术: 聚合分析决定 n {\displaystyle n} 个操作序列的耗费上界 T ( n ) {\displaystyle T(n)} ，然后计算平均耗费为 T ( n ) / n {\displaystyle T(n)/n} 。 记账方法确定每个操作的耗费，结合它的直接执行时间及它在对运行时中未来操作的影响。通常来说，许多短操作增量累加成「债」，而通过减少长操作的次数来「偿还」。 势能方法类似记账方法，但通过预先储蓄「势能」而在需要的时候释放。 計算n個操作的時間複雜度上限T(n) 平攤T(n)至每一個操作，每一個操作的平攤成本是T(n)/n 執行花費較低的operations時先存credit未雨綢繆, 供未來花費較高的operations使用 對每個操作定義一個合法的平攤成本(amortized cost) 假設 c i {\displaystyle c_{i}} 為第i個操作的actual cost， c i ^ {\displaystyle {\hat {c_{i}}}} 為第i個操作的amortized cost 若 c i < c i ^ {\displaystyle c_{i}<{\hat {c_{i}}}} ，則credit= c i ^ − c i {\displaystyle {\hat {c_{i}}}-c_{i}} ，我們把credit存起來(deposited)，未來可以提取(withdraw) 若 c i > c i ^ {\displaystyle c_{i}>{\hat {c_{i}}}} ，則提取credit 設定每個操作的平攤成本(amortized cost)後，要做valid check確保credit不可以是0，也就是說 ∑ k = 1 n c i ^ ≥ ∑ k = 1 n c i {\displaystyle \sum _{k=1}^{n}{\hat {c_{i}}}\geq \sum _{k=1}^{n}c_{i}} 定義一個位能函數(potential function) Φ ( D ) {\displaystyle \Phi (D)} ，將資料結構D(例如: 堆疊)的狀態對應到一個實數 D 0 {\displaystyle D_{0}} : 資料結構D的初始狀態 D i {\displaystyle D_{i}} : 資料結構D經過 i {\displaystyle i} 個操作後的狀態 c i {\displaystyle c_{i}} : 第 i {\displaystyle i} 個操作的actual cost c i ^ {\displaystyle {\hat {c_{i}}}} : 第 i {\displaystyle i} 個操作的amortized cost 定義 c i ^ = c i + Φ ( D i ) − Φ ( D i − 1 ) {\displaystyle {\hat {c_{i}}}=c_{i}+\Phi (D_{i})-\Phi (D_{i-1})} ∑ k = 1 n c i ^ = ∑ k = 1 n ( c i + Φ ( D i ) − Φ ( D i − 1 ) ) = ( ∑ k = 1 n c i ) + Φ ( D n ) − Φ ( D 0 ) {\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})} 為了滿足 ∑ k = 1 n c i ^ ≥ ∑ k = 1 n c i {\displaystyle \sum _{k=1}^{n}{\hat {c_{i}}}\geq \sum _{k=1}^{n}c_{i}} 我們定義 Φ ( D n ) − Φ ( D 0 ) ≥ 0 {\displaystyle \Phi (D_{n})-\Phi (D_{0})\geq 0} , 通常令 Φ ( D 0 ) = 0 {\displaystyle \Phi (D_{0})=0} 和 Φ ( D n ) ≥ 0 {\displaystyle \Phi (D_{n})\geq 0} 我們定義一個堆疊有下列操作 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)" 令 n p u s h {\displaystyle n_{push}} 是S.push(x)的執行次數， n p o p {\displaystyle n_{pop}} 是S.pop()的執行次數， n m u l t i − p o p {\displaystyle n_{multi-pop}} 是S.multi-pop(k)的執行次數 n = n p u s h + n p o p + n m u l t i − p o p {\displaystyle n=n_{push}+n_{pop}+n_{multi-pop}} 為總執行次數 因為一個堆疊S如果是空的，就不能執行pop了，也就是說可以pop或multi-pop的元素個數不會超過S中push進去的元素個數 所以 n p o p + n m u l t i − p o p ≤ n p u s h {\displaystyle n_{pop}+n_{multi-pop}\leq n_{push}} 假設 T ( n ) {\displaystyle T(n)} 是執行 n {\displaystyle n} 個操作的時間複雜度上限 T ( n ) = O ( 1 ) × n p u s h + O ( 1 ) × n p u s h = O ( n ) {\displaystyle T(n)=O(1)\times n_{push}+O(1)\times n_{push}=O(n)} 所以堆疊一個操作的平攤成本為 O ( n ) / n = O ( 1 ) {\displaystyle O(n)/n=O(1)} 我們假設S.push(x), S.pop(), S.multi-pop(k)的amortized cost分別為2, 0, 0，如下表所示 因此每個操作的平攤成本是O(1) 我們定義位能函數 Φ ( D ) {\displaystyle \Phi (D)} 為執行i個操作後，堆疊內的元素個數 Φ ( D 0 ) = 0 {\displaystyle \Phi (D_{0})=0} ，因為堆疊一開始是空的 Φ ( D i ) ≥ 0 {\displaystyle \Phi (D_{i})\geq 0} ，因為堆疊的元素個數一定 ≥ 0 {\displaystyle \geq 0} 計算堆疊S每一個操作的平攤成本 總平攤成本 ∑ k = 1 n c i ^ = 2 × n p u s h = O ( n ) {\displaystyle \sum _{k=1}^{n}{\hat {c_{i}}}=2\times n_{push}=O(n)} ，所以堆疊單一個操作的平攤成本是 O ( n ) / n = O ( 1 ) {\displaystyle O(n)/n=O(1)} 考虑一个随元素个数增加而增长的动态数组，比如Java的ArrayList或者C++的std::vector。如果我们的数组大小从4开始，那么来向其中增加四个元素的时间就是一个常数。然而，若要将第五个元素加入其中，那么会花费更多时间，因为我们此时必须要创建一个两倍于当前数组大小的数组（8个元素），把老元素拷贝到新数组中，然后增加一个新元素。接下来的三次加入操作也同样会花费常数时间，然后在数组被填满后则又需要一轮新的加倍扩充。 一般地，如果我们考虑任意一个任意的n大小的数组并对其进行n + 1次加入操作。我们注意到，所有的加入操作都是常数时间的，除了最后一个，它会花费 O ( n ) {\displaystyle O(n)} 时间在大小加倍上。因为我们进行了n + 1次加入操作，我们可以将数组加倍的时间平摊到所有的加入操作上，因此得到加入操作的平均时间是 n O ( 1 ) + O ( n ) n + 1 = O ( 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需要 O ( n ) {\displaystyle O(n)} 時間將所有元素從輸入數組添加到輸出數組中，其中n是輸入數組的當前長度。 從輸入複製n元素後，我們可以在輸出數組再次為空之前執行n出隊操作，每次操作都需要一個恆定的時間。 因此，我們可以僅在 o ( n ) {\displaystyle o(n)} 時間執行一系列n出列操作，這意味著每個出列操作的攤銷時間是 O ( 1 ) {\displaystyle O(1)} 。 或者，我們可以收取將任何項目從輸入數組複製到輸出數組的成本，以及該項目的早期排隊操作。 該計費方案將入隊的攤還時間加倍，但將出列的攤還時間減少到 O ( 1 ) {\displaystyle O(1)} 。 在常见场合，我们把能很好平摊分析的算法称为“平摊算法”。 在线算法通常使用平摊分析。 Kozen, Dexter. CS 3110 Lecture 20: Amortized Analysis. Cornell University. Spring 2011 [14 March 2015]. （原始内容存档于2018-10-03）.  Grossman, Dan. CSE332：Data Abstractions (PDF). cs.washington.edu. [2015年3月14日]. （原始内容存档 (PDF)于2015年4月2日）.  MIT 6.046J Design and Analysis of Algorithms, Spring 2015. MIT. [2018-10-21]. （原始内容存档于2018-11-25）.