均摊复杂度前置知识:时间复杂度
本页面将介绍均摊复杂度的基础知识.
引入均摊分析(Amortized Analysis)是一种用于分析算法和动态数据结构性能的技术.它不仅仅关注单次操作的成本,还通过评估一系列操作的平均成本,为整体性能提供更加准确的评估.均摊分析不涉及概率,且只能确保最坏情况性能的每次操作耗费的平均时间,并不能确认系统的平均性能.在最坏情况下,均摊分析通过将高成本操作的开销分摊到低成本操作上,确保整体操作的平均成本保持在合理范围内.
均摊分析通常采用三种主要分析方法:聚合分析、记账分析和势能分析.这些方法各有侧重,分别适用于不同的场景,但它们的共同目标是通过均衡操作成本,优化数据结构在最坏情况下的整体性能表现.
内容考虑一个可扩展的数组,例如 C++ 中的 vector,其初始容量为 𝑚 =1m=1.每次插入新元素时,如果数组已满,则需要将数组的大小加倍,然后将原数组中的元素复制到新数组中,最后插入新元素.
接下来,将以动态数组的插入操作为例,通过聚合分析、记账分析和势能分析三种方法,分析其均摊成本.
聚合分析聚合分析(Aggregate Analysis)通过计算一系列操作的总成本,并将其平均到每次操作上,从而得出每次操作的均摊时间复杂度.
以动态数组为例,首先,可以得到插入操作的两个关键成本:
如果数组未满,插入操作的成本为 𝑂(1)O(1).如果数组已满,则插入操作需要扩容,扩容后复制元素的成本为 𝑂(𝑚)O(m),其中 𝑚m 为当前数组的大小.所以,为了计算 𝑛n 次插入操作的总成本,可以将其分开为两部分计算:
插入操作的成本:每次插入新元素的直接成本是常数时间 𝑂(1)O(1),对于 𝑛n 次操作,总成本是 𝑂(𝑛)O(n);数组扩容的成本:每次扩容涉及到复制原数组元素到新数组.这些操作发生在数组大小为 22 的幂次时.扩容时复制的元素总数为所有严格小于 𝑛n 的二次幂之和,当 𝑛 ≥1n≥1 时,总复制成本为 ∑⌈log2𝑛⌉−1𝑖=02𝑖 =2⌈log2𝑛⌉ −1 <2𝑛∑i=0⌈log2n⌉−12i=2⌈log2n⌉−1<2n,总成本是 𝑂(𝑛)O(n).因此,该数组总的插入成本为 𝑂(𝑛)O(n),均摊到每次操作的成本为 𝑂(1)O(1).即使在最坏情况下,平均每次插入操作的成本依然是常数时间.
记账分析记账法(Accounting Method)通过为每次操作预先分配一个固定的均摊成本来确保所有操作的总成本不超过这些预分配的成本总和.记账法类似于一种 费用前置支付 的机制,其中较低成本的操作会存储部分费用,以支付未来高成本的操作.
以动态数组为例,可以为每次插入操作分配一个固定的均摊成本,以确保在需要扩容时已经预留了足够的费用.
费用分配:
假设每次插入操作的实际成本为 11,均摊成本设为 33.其中 11 用于当前插入操作,22 用于未来可能的扩容操作.费用使用:
当数组已满时,需要进行扩容操作,实际成本为 𝑂(𝑚)O(m),其中 𝑚m 是当前数组的大小.假设扩容前数组的元素数量为 𝑛n,由于原数组的后半部分 𝑛/2n/2 个元素在插入时共预存了 𝑛n 单位的均摊成本,恰好足够支付扩容操作的成本.以下是一个具体的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15初始状态:
arr = [1, 2, 3, 4] // 初始数组
amount = [2, 2, 2, 2] // 每个元素预存的费用
// 第一轮扩容:数组已满,需要扩容
arr = [1, 2, 3, 4, null, null, null, null] // 扩容后数组
amount = [2, 2, 0, 0, 0, 0, 0, 0] // 3, 4的费用用于支付扩容
// 继续插入新元素,直至再次满载
arr = [1, 2, 3, 4, 5, 6, 7, 8] // 继续填充数组
amount = [2, 2, 0, 0, 2, 2, 2, 2] // 新插入的元素同样预存费用
// 第二轮扩容:数组再次满载,需要更大的空间
arr = [1, 2, 3, 4, 5, 6, 7, 8, null, null, null, null, null, null, null, null] // 扩容后数组
amount = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // 5, 6, 7, 8的费用用于支付扩容
以上过程表明,每次插入操作所存储的均摊成本足够支付未来的扩容操作,从而确保了每次操作的均摊成本维持在 𝑂(1)O(1).
势能分析势能分析(Potential Method)通过定义一个势能函数(通常表示为 ΦΦ),度量数据结构的 潜在能量,即系统状态中的预留资源,这些资源可以用来支付未来的高成本操作.势能的变化用于平衡操作序列的总成本,从而确保整个算法的均摊成本在合理范围内.
原理首先,定义 状态 𝑆S 为某一时刻数据结构的状态,该状态可能包含元素数量、容量、指针等信息,其中定义初始状态为 𝑆0S0,即未进行任何操作时的状态.
其次,定义势能函数 Φ(𝑆)Φ(S) 用于度量数据结构状态 𝑆S 的势能,设初始状态为 𝑆0S0,则所有可达状态 𝑆S 的势能均需满足 Φ(𝑆) ≥Φ(𝑆0)Φ(S)≥Φ(S0)123,有的资料会额外要求 Φ(𝑆0) =0Φ(S0)=04,实际上是不必要的.
对于每个操作,其均摊成本 ˆ𝑐c^ 定义为:
ˆ𝑐=𝑐+Φ(𝑆′)−Φ(𝑆)c^=c+Φ(S′)−Φ(S)其中 𝑐c 为操作的实际成本,𝑆S 和 𝑆′S′ 分别表示操作前后的数据结构状态.该公式表明,均摊成本等于实际成本加上势能的变化.如果操作增加了势能(即 Φ(𝑆′) >Φ(𝑆)Φ(S′)>Φ(S)),则均摊成本上升;如果操作消耗了势能(即 Φ(𝑆′) <Φ(𝑆)Φ(S′)<Φ(S)),则均摊成本下降.
我们可以通过势能函数来分析一系列操作的总成本.设 𝑆1,𝑆2,…,𝑆𝑚S1,S2,…,Sm 为从初始状态 𝑆0S0 开始,经过 𝑚m 次操作后产生的状态序列,𝑐𝑖ci 为第 𝑖i 次操作的实际开销,那么第 𝑖i 次操作的均摊成本 𝑝𝑖pi 为:
𝑝𝑖=𝑐𝑖+Φ(𝑆𝑖)−Φ(𝑆𝑖−1)pi=ci+Φ(Si)−Φ(Si−1)因此,𝑚m 次操作的总时间花销为:
𝑚∑𝑖=1𝑐𝑖=𝑚∑𝑖=1𝑝𝑖+Φ(𝑆0)−Φ(𝑆𝑚)∑i=1mci=∑i=1mpi+Φ(S0)−Φ(Sm)由于 Φ(𝑆) ≥Φ(𝑆0)Φ(S)≥Φ(S0),总时间花销的上界为:
𝑚∑𝑖=1𝑝𝑖≥𝑚∑𝑖=1𝑐𝑖∑i=1mpi≥∑i=1mci因此,若 𝑝𝑖 =𝑂(𝑇(𝑛))pi=O(T(n)),则 𝑂(𝑇(𝑛))O(T(n)) 是均摊复杂度的一个上界.
示例:动态数组的扩容分析以动态数组 vector 的插入操作为例,记初始状态为 ℎ0h0,定义如下的势能函数 Φ(ℎ)Φ(h):
Φ(ℎ)=2𝑛−𝑚Φ(h)=2n−m其中,𝑛n 是数组中的元素数量,𝑚m 是数组的当前容量.这个势能函数的变化反映了用于支付未来扩容的预存费用的增减.在容量为 11 的空数组初态,Φ(ℎ0) = −1Φ(h0)=−1;在仅插入、满时倍增的非空可达状态中,𝑚 ≤2𝑛m≤2n,故 Φ(ℎ) ≥0 >Φ(ℎ0)Φ(h)≥0>Φ(h0).
插入操作(无需扩容):
操作成本:𝑂(1)O(1),因为只需插入一个元素.势能变化:插入后,元素数量增加 1,势能增加 22.Φ(ℎ′) −Φ(ℎ) =2(𝑛 +1) −𝑚 −(2𝑛 −𝑚) =2Φ(h′)−Φ(h)=2(n+1)−m−(2n−m)=2.均摊成本:1 +2 =31+2=3.插入操作(触发扩容):
假设当前容量 𝑚 =𝑛m=n,插入一个新元素时触发扩容,新的容量变为 2𝑛2n.操作成本:𝑂(𝑛)O(n),因为需要将所有元素复制到新数组中,并插入新元素.势能变化:扩容并插入后的势能变化为 2 −𝑛2−n,当 𝑛 >2n>2 时才下降.Φ(ℎ′) −Φ(ℎ) =2(𝑛 +1) −2𝑛 −(2𝑛 −𝑛) =2 −𝑛Φ(h′)−Φ(h)=2(n+1)−2n−(2n−n)=2−n.均摊成本:𝑛 +1 +(2 −𝑛) =3n+1+(2−n)=3.通过上述分析可以看出,尽管扩容操作的实际成本较高,但由于势能函数的设计,整体均摊成本仍然保持在常数级别 𝑂(1)O(1).
扩展示例:堆栈操作堆栈操作是均摊分析的经典应用场景之一.假设从空栈开始,pop 仅用于非空栈,堆栈 S 支持以下三种操作:
操作说明实际成本 𝑐𝑖ciS.push(x)将元素 𝑥x 入栈11S.pop()弹出栈顶元素11S.multi-pop(k)最多弹出栈顶 𝑘k 个元素𝑂(min(|𝑆|,𝑘))O(min(|S|,k))我们将通过聚合分析、记账分析和势能分析三种方法来分析这些堆栈操作的均摊成本.
聚合分析法聚合分析将计算所有操作的总成本,并将其平均分摊到每个操作上,从而得出均摊成本.
对于 𝑛pushnpush 次 push(x) 操作,每次的成本为 𝑂(1)O(1),因此总成本为 𝑂(𝑛push)O(npush);对于 𝑛popnpop 次 pop() 操作,每次的成本为 𝑂(1)O(1),总成本为 𝑂(𝑛pop)O(npop);对于 𝑛multi-popnmulti-pop 次 multi-pop(k) 操作,尽管每次的实际成本为 𝑂(min(|𝑆|,𝑘))O(min(|S|,k)),但这些操作弹出的元素数量不会超过之前 push(x) 的元素数量,因此总成本仍受 𝑛pushnpush 的约束.总操作次数为 𝑛 =𝑛push +𝑛pop +𝑛multi-popn=npush+npop+nmulti-pop,且 𝑛push ≤𝑛npush≤n.每个元素最多入栈和出栈各一次,故按元素计费的总成本为 𝑂(𝑛push) =𝑂(𝑛)O(npush)=O(n);即使另计每次调用的常数检查开销,总成本仍为 𝑂(𝑛)O(n),均摊为 𝑂(1)O(1).
记账分析法记账分析为每次 push(x) 操作预留一部分费用,以支付未来可能的 pop() 或 multi-pop(k) 操作.
S.push(x):假设每次 push(x) 操作的均摊成本为 22,其中 11 单位用于当前操作,另 11 单位存储为费用,用于支付未来的 pop() 或 multi-pop(k) 操作;S.pop():实际成本为 11,但由于之前的 push(x) 操作已为其预存了 11 单位费用,因此均摊成本为 00;S.multi-pop(k):每个弹出的元素的实际成本为 11,可以由之前该元素的 push(x) 操作预存的费用支付,因此均摊成本为 00.通过以上分析,入栈操作预存的费用足以支付未来该元素的出栈操作,因此每次操作的均摊成本为 𝑂(1)O(1).
势能分析法势能分析定义了一个势能函数来衡量堆栈的状态,并利用势能的变化来平衡操作成本.
势能函数:设 Φ(ℎ)Φ(h) 为堆栈中的元素数量,即 Φ(ℎ) =|𝑆|Φ(h)=|S|.每个元素贡献 11 单位的势能;S.push(x):每次 push(x) 操作增加堆栈中的元素数量,势能增加 11,因此均摊成本为 1 +1 =21+1=2;S.pop():每次 pop() 操作减少堆栈中的元素数量,势能减少 11,因此均摊成本为 1 −1 =01−1=0;S.multi-pop(k):令 𝑡 =min(|𝑆|,𝑘)t=min(|S|,k),multi-pop(k) 实际弹出 𝑡t 个元素,势能减少 𝑡t,按元素计费的均摊成本为 𝑡 −𝑡 =0t−t=0;计入调用开销则为 𝑂(1)O(1).通过以上势能函数设计,push(x) 操作的均摊成本为 22,而 pop() 和 multi-pop(k) 操作的均摊成本为 00.因此,所有堆栈操作的均摊成本均为 𝑂(1)O(1).
参考资料与注释Amortized Analysis - WikipediaClemson University - Amortized Analysis + Splay Trees ↩
UC Irvine - Amortized Analysis ↩
Stanford CS166 - Amortized Analysis ↩
Cornell CS 3110 - Lecture 20: Amortized Analysis ↩
本页面最近更新:2026/11/4 20:10:15,更新历史发现错误?想一起完善? 在 GitHub 上编辑此页!本页面贡献者:Tiphereth-A, c-forrest, yyyu-star, HeRaNO, jyeric, not-exist本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用