方法
累计分析
所有操作的时间和除以操作次数。
势能分析
每一个实际代价 \(c_i\) 都将数据结构从 \(D_{i-1}\) 改变为 \(D_i\),定义平摊代价为:
\(φ\) 代表势能函数。
如果平摊代价要比实际代价大,那么我们就说势能增加,否则势能降低。
将所有操作的平摊代价相加可以得到平摊代价的和 \(=\) 实际代价和 \(+φ(D_n)-φ(D_0)\)。
假设 \(φ(D_0)\) 为 \(0\)。如果我们能证明 \(φ(D_n)\) 非负,那么我们就可以说明我们的平摊代价一定是实际代价的一个上界。
例子
二进制计数器
描述
有一个初始值为 \(0\) 的 \(k\) 位二进制计数器只支持加一操作,把改变一个二进制位看做常数时间。求 \(n\) 次操作的时间复杂度。
累计分析
不考虑每一次,而是考虑每一位。
考虑第 \(0\) 位变化 \(n\) 次,第 \(1\) 位变化 \(n/2\) 次,依此类推。总变化次数是 \(2n\) 级别的。
势能分析
令势能函数为计数器中 \(1\) 的个数。那么 \(φ(D_n)\) 为正。
考虑一次变化。设有 \(x\) 位 \(1\rightarrow 0\),而有 \(1\) 位 \(0\rightarrow 1\)(容易发现一次 \(0\rightarrow 1\) 后不会进位)。势能函数变化量 \(1-x\),而实际代价 \(x+1\),相加得到平摊代价 \(2\)。
栈
描述
一个一次可删除 \(k\) 个元素的栈。
势能分析
令势能函数为栈内元素的个数。那么 \(φ(D_n)\ge 0\)。
进栈平摊代价 \(2\),出栈为 \(0\)。
动态表
描述
一个数组,假设其大小为 \(size_T\),元素个数为 \(num_T\)。初始时 \(size_T=1\)。
当这个数组满了的时候,我们将这个数组扩大一倍,使其能接着存储。
实际代价:在 \(2^i\) 步代价为 \(2^i+1\),其余代价为 \(1\)。
累计分析
将每一步都取一个 \(1\) 出来,然后将剩下的等比数列求和:
均摊代价为 \(3\)。
势能分析
考虑势能函数的目的就是为了保证在进行大代价操作时的代价能被多余的势能抵消。我们想满足这样的条件:
- 刚扩充完。\(φ(T)=0\)。
- 表满时。\(φ(T)=size_T\)。
令势能函数为 \(2\times num_T - size_T\)。那么 \(φ(D_n)\ge 0\)。
对于不扩张的操作,势能增加 \(2\),实际代价为 \(1\),按照公式平摊代价为 \(3\)。
对于需要扩张的操作,\(size_T\) 扩大为两倍,\(num_T\) 加一。所以势能差为 \(2(num_T+1) - 2size_T - (2num_T - size_T) = 2-size_T\),而我们的实际代价为 \(1+size_T\),求和仍为 \(3\),得证。