均摊复杂度
前置知识:时间复杂度
本页面将介绍均摊复杂度的基础知识。
引入
均摊分析(Amortized Analysis)是一种用于分析算法和动态数据结构性能的技术。它不仅仅关注单次操作的成本,还通过评估一系列操作的平均成本,为整体性能提供更加准确的评估。均摊分析不涉及概率,且只能确保最坏情况性能的每次操作耗费的平均时间,并不能确认系统的平均性能。在最坏情况下,均摊分析通过将高成本操作的开销分摊到低成本操作上,确保整体操作的平均成本保持在合理范围内。
均摊分析通常采用三种主要分析方法:聚合分析、记账分析和势能分析。这些方法各有侧重,分别适用于不同的场景,但它们的共同目标是通过均衡操作成本,优化数据结构在最坏情况下的整体性能表现。
内容
考虑一个可扩展的数组,例如 C++ 中的 vector
,其初始容量为
接下来,将以动态数组的插入操作为例,通过聚合分析、记账分析和势能分析三种方法,分析其均摊成本。
聚合分析
聚合分析(Aggregate Analysis)通过计算一系列操作的总成本,并将其平均到每次操作上,从而得出每次操作的均摊时间复杂度。
以动态数组为例,首先,可以得到插入操作的两个关键成本:
- 如果数组未满,插入操作的成本为
。 - 如果数组已满,则插入操作需要扩容,扩容后复制元素的成本为
,其中 为当前数组的大小。
所以,为了计算 n 次插入操作的总成本,可以将其分开为两部分计算:
- 插入操作的成本:每次插入新元素的直接成本是常数时间
,对于 次操作,总成本是 。 - 数组扩容的成本:每次扩容涉及到复制原数组元素到新数组。这些操作发生在数组大小为
的时刻,其中 是小于等于 的最大幂。扩容操作的成本分别是 ,总和为 ,这是一个等比数列的和,其结果为 。
因此,该数组总的插入成本为
记账分析
记账法(Accounting Method)通过为每次操作预先分配一个固定的均摊成本来确保所有操作的总成本不超过这些预分配的成本总和。记账法类似于一种 费用前置支付 的机制,其中较低成本的操作会存储部分费用,以支付未来高成本的操作。
以动态数组为例,可以为每次插入操作分配一个固定的均摊成本,以确保在需要扩容时已经预留了足够的费用。
-
费用分配:
- 假设每次插入操作的实际成本为
,均摊成本设为 。 - 其中
用于当前插入操作, 用于未来可能的扩容操作。
- 假设每次插入操作的实际成本为
-
费用使用:
- 当数组已满时,需要进行扩容操作,实际成本为
,其中 是当前数组的大小。 - 假设扩容前数组的元素数量为
,由于原数组的后半部分 个元素在插入时共预存了 单位的均摊成本,恰好足够支付扩容操作的成本。
- 当数组已满时,需要进行扩容操作,实际成本为
以下是一个具体的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
以上过程表明,每次插入操作所存储的均摊成本足够支付未来的扩容操作,从而确保了每次操作的均摊成本维持在
势能分析
势能分析(Potential Method)通过定义一个势能函数(通常表示为
原理
首先,定义 状态
其次,定义势能函数
- 初始势能:在数据结构的初始状态
下,势能 。 - 非负性:在任意状态
下,势能 。
对于每个操作,其均摊成本
其中
我们可以通过势能函数来分析一系列操作的总成本。设
因此,
由于
因此,若
示例:动态数组的扩容分析
以动态数组 vector
的插入操作为例,定义如下的势能函数
其中,
-
插入操作(无需扩容):
- 操作成本:
,因为只需插入一个元素。 - 势能变化:插入后,元素数量增加 1,势能增加
。 - 均摊成本:
- 操作成本:
-
插入操作(触发扩容):
- 假设当前容量
,插入一个新元素时触发扩容,新的容量变为 。 - 操作成本:
,因为需要将所有元素复制到新数组中,并插入新元素。 - 势能变化:扩容后,容量增加,势能减少,变化大小为
。 - 均摊成本:
- 假设当前容量
通过上述分析可以看出,尽管扩容操作的实际成本较高,但由于势能函数的设计,整体均摊成本仍然保持在常数级别
扩展示例:堆栈操作
堆栈操作是均摊分析的经典应用场景之一。假设堆栈 S
支持以下三种操作:
操作 | 说明 | 实际成本 |
---|---|---|
S.push(x) |
将元素 x 入栈 | |
S.pop() |
弹出栈顶元素 | |
S.multi-pop(k) |
弹出栈顶 k 个元素 |
我们将通过聚合分析、记账分析和势能分析三种方法来分析这些堆栈操作的均摊成本。
聚合分析法
聚合分析将计算所有操作的总成本,并将其平均分摊到每个操作上,从而得出均摊成本。
- 对于
次push(x)
操作,每次的成本为 ,因此总成本为 。 - 对于
次pop()
操作,每次的成本为 ,总成本为 。 - 对于
次multi-pop(k)
操作,尽管每次的实际成本为 ,但这些操作弹出的元素数量不会超过之前push(x)
的元素数量,因此总成本仍受 的约束。
由于总操作次数
记账分析法
记账分析为每次 push(x)
操作预留一部分费用,以支付未来可能的 pop()
或 multi-pop(k)
操作。
S.push(x)
:假设每次push(x)
操作的均摊成本为 ,其中 单位用于当前操作,另 单位存储为费用,用于支付未来的pop()
或multi-pop(k)
操作。S.pop()
:实际成本为 ,但由于之前的push(x)
操作已为其预存了 单位费用,因此均摊成本为 。S.multi-pop(k)
:每个弹出的元素的实际成本为 ,可以由之前该元素的push(x)
操作预存的费用支付,因此均摊成本为 。
通过以上分析,入栈操作预存的费用足以支付未来该元素的出栈操作,因此每次操作的均摊成本为
势能分析法
势能分析定义了一个势能函数来衡量堆栈的状态,并利用势能的变化来平衡操作成本。
- 势能函数:设
为堆栈中的元素数量,即 。每个元素贡献 单位的势能。 S.push(x)
:每次push(x)
操作增加堆栈中的元素数量,势能增加 ,因此均摊成本为 。S.pop()
:每次pop()
操作减少堆栈中的元素数量,势能减少 ,因此均摊成本为 。S.multi-pop(k)
:multi-pop(k)
操作弹出 个元素,势能减少 ,因此均摊成本为 。
通过以上势能函数设计,push(x)
操作的均摊成本为 pop()
和 multi-pop(k)
操作的均摊成本为
参考资料
本页面最近更新:2024/9/23 09:40:36,更新历史
发现错误?想一起完善? 在 GitHub 上编辑此页!
本页面贡献者:HeRaNO, yyyu-star
本页面的全部内容在 CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用