普通的动态表问题
我们需要用一个数据结构存储一些数据,但并不知道数据有多少个,所以也不知道申请多少空间合适。因此,我们采取了动态申请空间的策略,假设现在表的大小为
s
i
z
e
size
size,数据数为
n
u
m
num
num。
若
n
u
m
<
s
i
z
e
num<size
num<size时需要添加一个数据,直接加入即可,单次操作复杂度为1。
当
n
u
m
=
s
i
z
e
num=size
num=size时需要添加一个数据,则申请一个大小为
2
s
i
z
e
2size
2size的空间,然后将原来的
n
u
m
num
num个数加上新加的数据都放进新空间里,复杂度为
n
u
m
+
1
num+1
num+1。
假设插入
n
n
n次,那么这样一个数据结构的均摊复杂度是多少呢?
朴素的分析是,单次操作是
O
(
n
)
O(n)
O(n)的,操作数是
O
(
n
)
O(n)
O(n)的,所以是
O
(
n
2
)
O(n^2)
O(n2)。
这是一个很松的上界,可以做更好的估计。
采用聚集分析的话,当
i
−
1
i-1
i−1为2的次幂时,
c
i
=
i
c_i=i
ci=i。其他时候
c
i
=
1
c_i=1
ci=1。
所以总代价
∑
i
=
0
n
c
i
≤
n
+
∑
i
=
1
⌊
log
n
⌋
2
i
≤
3
n
\sum_{i=0}^nc_i \leq n+\sum_{i=1}^{\lfloor \log n \rfloor} 2^i \leq 3n
∑i=0nci≤n+∑i=1⌊logn⌋2i≤3n
平摊代价
3
n
n
=
3
\frac{3n}{n}=3
n3n=3。
有了这个启发后,我们还可以设计记账法来分析。
一次插入的记账为3,其中1的代价给这次新插的数用,1的代价存款在新插的数上,1的代价给一个没有存款的数据。一次扩张后到下一次扩张前,新插的数据的个数和旧的数据个数是相等的,所以每次扩张时,动态表内的所有数上均有1的存款,那么换空间的复杂度只需要消耗这些存款了。
最后,考虑势函数法。
决定势函数的只有两个东西,
s
i
z
e
size
size和
n
u
m
num
num,不妨先待定系数,设
Φ
(
T
)
=
a
n
u
m
+
b
s
i
z
e
\Phi(T)=anum+bsize
Φ(T)=anum+bsize
首先,我们考虑触发表扩张的一个操作,需要让它的平摊代价为常数。这里的
n
u
m
num
num和
s
i
z
e
size
size设为添加新数据前的:
a
i
=
c
i
+
Φ
(
T
i
)
−
Φ
(
T
i
−
1
)
=
n
u
m
+
1
+
(
a
(
n
u
m
+
1
)
+
b
(
2
s
i
z
e
)
)
−
(
a
n
u
m
+
b
s
i
z
e
)
=
n
u
m
+
1
+
a
+
b
s
i
z
e
a_i=c_i+\Phi(T_i)-\Phi(T_{i-1})=num+1+(a(num+1)+b(2size))-(anum+bsize)=num+1+a+bsize
ai=ci+Φ(Ti)−Φ(Ti−1)=num+1+(a(num+1)+b(2size))−(anum+bsize)=num+1+a+bsize
扩张前应该有
n
u
m
=
s
i
z
e
num=size
num=size,所以
b
=
−
1
b=-1
b=−1时是常数。
至于这个常数是多少,其实我们用聚集分析和记账法已经算出来了,是3,所以
a
=
2
a=2
a=2。
得到势函数:
Φ
(
T
)
=
2
n
u
m
−
s
i
z
e
\Phi(T)=2num-size
Φ(T)=2num−size
验证一下不扩张的情况:
a
i
=
1
+
(
2
n
u
m
+
2
−
s
i
z
e
)
−
(
2
n
u
m
−
s
i
z
e
)
=
3
a_i=1+(2num+2-size)-(2num-size)=3
ai=1+(2num+2−size)−(2num−size)=3
完美!
带删除的动态表问题
在上面的例子看起来,我们可以利用聚集分析和记账法完成,势函数显得有些没必要。但这个问题升级一下,前两种方法就不是那么好用了。
假设我们的动态表中的数据可以被删除,则这个表可能也需要收缩。既然我们扩张都是2的倍数,那当然收缩也收缩成
1
2
\frac{1}{2}
21比较好。
定义装载率
α
=
n
u
m
s
i
z
e
\alpha=\frac{num}{size}
α=sizenum,什么时候收缩呢?
如果是
α
=
1
2
\alpha=\frac{1}{2}
α=21时收缩,则可能出现一种情况,删除和插入交错进行,表的大小反复横跳。所以我们会定一个值,比如说
α
0
=
1
4
\alpha_0=\frac{1}{4}
α0=41时收缩。
势函数不应该小于0,所以应该分两段(这里以
1
2
\frac{1}{2}
21分段是因为扩张和收缩的分界点都是2次幂,如果是3次幂扩张等情况则应该不同考虑)来设计:
Φ
(
T
)
=
{
a
n
u
m
+
b
s
i
z
e
α
≥
1
2
c
n
u
m
+
d
s
i
z
e
α
<
1
2
\Phi(T)=\begin{cases}anum+bsize & \alpha \geq \frac{1}{2} \\ cnum+dsize & \alpha < \frac{1}{2} \end{cases}
Φ(T)={anum+bsizecnum+dsizeα≥21α<21
在
1
2
\frac{1}{2}
21处势函数应该连续,所以:
a
n
u
m
+
2
b
n
u
m
=
c
n
u
m
+
2
d
n
u
m
anum+2bnum=cnum+2dnum
anum+2bnum=cnum+2dnum即
2
(
d
−
b
)
=
a
−
c
2(d-b)=a-c
2(d−b)=a−c。
构造型待定系数的解往往不止一种,不妨让
b
=
−
1
2
a
,
d
=
−
1
2
d
b=-\frac{1}{2}a,d=-\frac{1}{2}d
b=−21a,d=−21d。
接下来就是保证扩张和收缩的时刻的平摊代价都是常数。
扩张时:
n
u
m
+
1
+
(
a
n
u
m
+
a
+
2
b
s
i
z
e
)
−
(
a
n
u
m
+
b
s
i
z
e
)
num+1+(anum+a+2bsize)-(anum+bsize)
num+1+(anum+a+2bsize)−(anum+bsize),如前面一样的式子,可以解出
b
=
−
1
b=-1
b=−1所以
a
=
2
a=2
a=2
收缩时:
n
u
m
+
1
+
(
c
n
u
m
−
c
+
1
2
d
s
i
z
e
)
−
(
c
n
u
m
+
d
s
i
z
e
)
num+1+(cnum-c+\frac{1}{2}dsize)-(cnum+dsize)
num+1+(cnum−c+21dsize)−(cnum+dsize),将
α
0
=
1
4
\alpha_0=\frac{1}{4}
α0=41代入得
d
=
1
2
d=\frac{1}{2}
d=21
最终设计的势函数:
Φ
(
T
)
=
{
2
n
u
m
−
s
i
z
e
α
≥
1
2
1
2
s
i
z
e
−
n
u
m
α
<
1
2
\Phi(T)=\begin{cases}2num-size & \alpha \geq \frac{1}{2} \\ \frac{1}{2}size-num & \alpha < \frac{1}{2} \end{cases}
Φ(T)={2num−size21size−numα≥21α<21
知晓了势函数的设计原理后,即使问题继续变化,我们还是很好设计势函数。例如 α 0 = 1 3 \alpha_0=\frac{1}{3} α0=31时收缩呢?只需要在解收缩时的势函数方程时稍加变动。
首先,若
α
0
=
1
3
\alpha_0=\frac{1}{3}
α0=31时收缩,收缩后
α
=
2
3
>
1
2
\alpha=\frac{2}{3}>\frac{1}{2}
α=32>21,所以:
c
i
=
n
u
m
+
1
+
(
2
n
u
m
−
2
−
1
2
s
i
z
e
)
−
(
d
s
i
z
e
−
2
d
n
u
m
)
=
(
3
+
2
d
)
n
u
m
−
(
1
2
+
d
)
s
i
z
e
−
2
c_i=num+1+(2num-2-\frac{1}{2}size)-(dsize-2dnum)=(3+2d)num-(\frac{1}{2}+d)size-2
ci=num+1+(2num−2−21size)−(dsize−2dnum)=(3+2d)num−(21+d)size−2
s
i
z
e
=
3
n
u
m
size=3num
size=3num代入,解得
d
=
3
2
d=\frac{3}{2}
d=23,故
c
=
−
3
c=-3
c=−3