最差时间复杂度(Worst-case time per operation)
在我们需要考虑实时响应效率的时候很重要。
平均时间复杂的(Average-case Analysis)
对于一些可能的分布,计算特别的值或者大概率的上限。有时很难计算,并且不够准确。
平摊分析(Amortized analysis)
平摊分析的初衷是为了弥补最差时间复杂度过于悲观的不足。在平摊分析中,执行一系列数据结构操作所需要的时间是通过执行的所有操作求平均而得出的。平摊分析可以用来证明在一系列操作中,通过对所有操作求平均之后,即使其中单一的操作具有较大的代价,平均代价还是很小的。
平摊分析不牵涉到概率, 平摊分析保证在最坏情况下,每个操作具有的平均性能。
平摊分析的三种方法
1.聚集分析(aggregate analysis)
证明对所有的n, 由n个操作所构成的序列的总时间在最坏情况下为T(n)
例子:
栈操作
PUSH(S, x):将x压入S
POP(S):弹出栈顶
MULTIPOP(S, k):弹出栈顶k个对象
由n个PUSH, POP和MULTIPOP操作序列,其作用于一个初始为空的栈。
分析:每个操作的最坏情况时间是O(n). 因此n个操作的序列的代价是O (n2)
虽然这一分析正确,但这个界不够紧确,利用聚集分析,可以获得更好的上界。
一个对象在每次被压入栈后,至多被弹出一次。所以,调用POP(包括MULTIPOP)的次数至多等于PUSH的次数,即至多为 n n 。对任意的,包含 n n 个PUSH, POP, MULTIPOP操作的序列的总时间为. 每个操作的平均代价为 O(n)/n=O(1) O ( n ) / n = O ( 1 ) 。 聚集分析中,将每个操作的平摊代价指派为平均代价。所以三个栈操作的平摊代价都是 O(1) O ( 1 ) 。
2.记帐方法
在平摊分析的记帐方法中,对不同的操作赋予不同的费用,某些操作的费用比它们的实际代价或多或少。
我们对一个操作的收费的数量称为平摊代价。当一个操作的平摊代价超过了它的实际代价时,两者的差值就被当作存款(credit),并赋予数据结构中的一些特定对象,可以用来补偿那些平摊代价低于其实际代价的操作。这种方法与聚集分析不同的是,对后者,所有操作都具有相同的平摊代价。
数据结构中存储的总存款等于总的平摊代价和总的实际代价之差。注意:总存款不能是负的。
例子:
栈操作
实际代价:
PUSH 1
POP 1
MULTIPOP min(k, s)
现在对它们赋予以下的平摊代价:
PUSH: 2
POP: 0
MULTIPOP 0
假设用1元钱来表示代价的单位。开始时栈时空的。
栈数据结构与餐厅中一堆叠放的盘子类似。当把一个盘子压入栈时,用1元钱来支付该动作的实际代价,还有1元的存款,将其放在盘子的顶上。盘子上面的钱时用来预付将盘子从栈中弹出所需代价的。当执行一个POP操作时,对该操作不收取任何费用,只需用盘中存放的存款来支付其实际代价即可。同理,也无需对MULTIPOP收取任何费用。
因为栈上的每个盘子上面都有1元钱,而且栈中总有非负个数的盘子。这就保证了存款的总量总是非负的。这样,对任意的包含n次PUSH, POP, MULTIPOP操作的序列,总的平摊代价就是总的实际代价的一个上界。总的平摊代价为O(n), 故总的实际代价也为O(n).
3.势能方法(potential method)
在平摊分析中,势能方法(potential method)不是将已预付的工作作为存在数据结构特定对象中存款来表示,而是表示成一 种“势能”或“势”,它在需要时可以释放出来,永远是大于等于0的,用以支付后面的操作。势是与整个数据结构而不是其中的个别对象发生联系的。
对一个初始数据结构 D0 D 0 执行 n n 个操作。对每个,设 ci c i 为每个操作的实际代价,
Di D i 为对数据结构 Di−1 D i − 1 执行第 i i 个操作的结果. 势函数 将每个数据结构 Di D i 映射为一个实数 Φ(Di) Φ ( D i ) , 即与Di相联系的势. 第 i i 个操作的平摊代价根据势函数 Φ Φ 定义为
每个操作的平摊代价为其实际代价 ci c i 加上由于该操作所增加的势。
n个操作的总的平摊代价为
如果我们能定义一个势函数 Φ Φ 使得对所有 n n ,有, 则总的平摊代价 ∑ai ∑ a i 就是总的实际代价 ∑ci ∑ c i 的一个上界。通常为了方便起见定义 Φ(D0)=0 Φ ( D 0 ) = 0 。
直观上看,如果第 i i 个操作的势差是正的,则平摊代价 ai a i 表示对第 i i 个操作多收了费,同时数据结构的势也随之增加了。如果势差是负值,则平摊代价就表示对第个操作的少收费,这是通过减少势来支付该操作的实际代价。
平摊代价依赖于所选择的势函数 Φ Φ 。不同的势函数可能会产生不同的平摊代价,但它们都是实际代价的上界。最佳势函数的选择取决于所需的时间界。
例子:栈操作
定义栈上的势函数 Φ Φ 为栈中对象的个数。开始时要处理的空栈,且 Φ(D0)=0 Φ ( D 0 ) = 0 。因为栈中的对象数始终时非负的,故在第 i i 个操作后,栈Di就具有非负的势。
以表示的 n n 个操作的平摊代价的总和就表示了总的实际代价的一个上界。
计算各个操作的平摊代价:
- 对于于一个包含个对象的栈上的第 i i 个操作是PUSH:
则势差为
这个PUSH操作的平摊代价为 ai=ci+Φ(Di)-Φ(Di−1)=1+1=2 a i = c i + Φ ( D i ) - Φ ( D i − 1 ) = 1 + 1 = 2
- 假设栈上的第i个操作是MULTIPOP(S, k). 该操作的实际代价为min(s, k).
势差为 Φ(Di)-Φ(Di−1)=−min(s,k) Φ ( D i ) - Φ ( D i − 1 ) = − m i n ( s , k )
因此MULTIPOP操作的平摊代价为
ai=ci+Φ(Di)-Φ(Di−1)=min(s,k)−min(s,k)=0 a i = c i + Φ ( D i ) - Φ ( D i − 1 ) = m i n ( s , k ) − m i n ( s , k ) = 0
- 同理POP操作的平摊代价也是0。
三中栈操作的每一种的平摊代价都是 O(1) O ( 1 ) , 这样包含 n n 个操作的序列的总平摊代价就是。已经证明 n n 个操作的平摊代价的总和是总的实际代价的一个上界。所以个操作的最坏情况代价为 O(n) O ( n ) .
例子:
动态表,当向满的表插入一项时,将表扩大一倍,但当删除一项而引起表不足1/4满时(不是1/2),就将表缩写为原来的一半。我们的理想状态就是实际元素数量是动态表长度的一半。
令
Φ=|2L−|B||
Φ
=
|
2
L
−
|
B
|
|
可以利用平摊分析,证明插入和删除操作的平摊代价均为O(1).
- 对于插入操作
当不需要扩容的时候,可以正常插入,时间复杂度为 O(1) O ( 1 ) 。当需要扩容时,对于常数 c c 此时,因为要复制到新的动态表中,还要增加新元素。
而对于势能函数
那么均摊时间为
- 对于删除操作
当不需要缩容的时候,可以正常删除,时间复杂度为 O(1) O ( 1 ) 。当需要缩容时,对于常数 c c 此时,因为要复制到新的动态表中,还要增加新元素。
而对于势能函数
那么均摊时间为