HNU2026-算法设计与分析-笔记 3 摊还分析

3 摊还分析

什么是摊还分析

摊还分析(Amortized Analysis)关注的是一系列操作的平均最坏情况代价,而不是单次操作的最坏代价。

一个直观的比喻:看一个人的一生而非某一天。某天犯了大错(代价很高),但摊开到整个人生中,单次 " 失利 " 并不显得那么严重。

与其他分析方式的区别

分析方式 关注对象 是否使用概率
最坏情况分析 单次操作的理论最坏代价
平均情况分析 所有可能输入下的期望代价 是(需要假设输入分布)
摊还分析 一系列操作中单次操作的平均最坏代价

关键区分:

  • 摊还分析 ≠ 最坏时间复杂度(后者看单次,前者看 nnn 次操作的平均)
  • 摊还分析 ≠ 平均情况分析(后者考虑输入的概率分布,前者对所有输入都成立)
  • 摊还分析不使用概率,它给出的是确定性的上界

引入示例:动态数组

Java ArrayList 的扩容机制

ArrayList 本质是一个连续内存的动态数组。分配多少空间是一个两难问题:容量过小导致频繁扩容,容量过大浪费内存。

ArrayList 的策略:

  • 初始容量:首次插入时分配默认值 C0=10C_0 = 10C0=10
  • 扩容触发:当 size == capacity
  • 扩容倍数:新容量 ≈ 旧容量 × 1.5(下取整)

当触发扩容时:分配 1.5 倍大的新表 → 复制旧表所有元素 → 释放旧表。

代价分析(以 1.5 倍扩容为例):

第 iii 次插入 1 2 ... 10 11 12 ... 15 16
容量 10 10 ... 10 15 15 ... 15 22
代价 cic_ici 1 1 ... 1 11 1 ... 1 16

大多数插入代价为 1,偶尔一次扩容代价很高。

动态表的 2 倍扩容(教材 17.4 节)

这是教材中的标准示例,扩容倍数为 2:

iii 1 2 3 4 5 6 7 8 9 10
容量 1 2 4 4 8 8 8 8 16 16
cic_ici 1 2 3 1 5 1 1 1 9 1

扩容发生在第 1、2、3、5、9 次插入时,代价分别为 1、2、3、5、9(等于 "1 + 旧容量 ")。

三种摊还分析方法

一、聚合法(Aggregate Method)

最直接的方法:先算出 nnn 次操作的总代价上界 T(n)T(n)T(n),再除以 nnn 得到每次操作的摊还代价。

以 2 倍扩容的动态表为例,nnn 次插入的总代价:

T(n)=∑i=1nci=n+∑j=0⌊log⁡2n⌋2j<n+2n=3nT(n) = \sum_{i=1}^{n} c_i = n + \sum_{j=0}^{\lfloor \log_2 n \rfloor} 2^j < n + 2n = 3nT(n)=i=1∑nci=n+j=0∑⌊log2n⌋2j<n+2n=3n

第一项 nnn 是每次插入本身的代价。第二项是所有扩容的复制代价之和:1+2+4+⋯+2⌊log⁡n⌋<2n1 + 2 + 4 + \cdots + 2^{\lfloor \log n \rfloor} < 2n1+2+4+⋯+2⌊logn⌋<2n。

所以每次操作的摊还代价 = T(n)/n<3T(n)/n < 3T(n)/n<3,即 O(1)O(1)O(1)。

二、核算法(Accounting Method)

核心思想是 " 预收费 ":给每次操作分配一个摊还代价(可以高于或低于实际代价),多收的部分作为 " 存款 " 存在数据结构上,日后用于支付高代价操作。

只要保证任何时刻存款余额 ≥ 0,总摊还代价就是总实际代价的上界:

∑i=1nc^i≥∑i=1nci\sum_{i=1}^{n} \hat{c}i \geq \sum{i=1}^{n} c_ii=1∑nc^i≥i=1∑nci

2 倍扩容的例子

给每次插入收取 3 元:

  • 1 元:支付本次插入
  • 2 元:存入 " 银行 ",预留给未来扩容时的元素迁移

扩容时需要把旧表所有元素复制到新表。每个旧元素的迁移费用由它自己插入时预存的 2 元支付:1 元负责移动自己,另 1 元负责 " 帮 " 一个在上次扩容前就存在的老元素。

具体过程:大小为 8 的表插入前 4 项后存款耗尽,插入第 5 项时收 3 元、存 2 元。插入到第 8 项时积累 8 元存款。插入第 9 项触发扩容(移动 8 个旧元素),8 元存款刚好用完。如此反复。存款余额始终 ≥ 0。

所以每次插入的摊还成本为 3,即 O(1)O(1)O(1)。nnn 次插入总代价至多 3n=O(n)3n = O(n)3n=O(n)。

1.5 倍扩容(ArrayList)

每次插入收取 4 元(或 5 元以完全覆盖奇数扩容的零头):

  • 1 元用于本次插入
  • 3 元预存:扩容时从容量 NNN 扩到 1.5N1.5N1.5N,需迁移 NNN 个元素。其中 0.5N0.5N0.5N 个是上次扩容后新插入的(新项),NNN 个是旧项。由于旧项数量约为新项的 2 倍,3 元里 1 元给新项、2 元给旧项,刚好平衡。

由于整数下取整的原因,遇到奇数容量扩容时会有 1 元的微小亏空。解决办法:当容量为奇数时额外收 1 元补上,或直接把每次收费提到 5 元。无论取 4 还是 5,单次代价都是 O(1)O(1)O(1)。

三、势能法(Potential Method)

与核算法类似,但把 " 存款 " 抽象为整个数据结构的势能函数 Φ\PhiΦ。

定义:

  • DiD_iDi 为第 iii 次操作后数据结构的状态
  • 第 iii 次操作的摊还代价 c^i=ci+Φ(Di)−Φ(Di−1)\hat{c}i = c_i + \Phi(D_i) - \Phi(D{i-1})c^i=ci+Φ(Di)−Φ(Di−1)

总摊还代价:

∑i=1nc^i=∑i=1nci+Φ(Dn)−Φ(D0)\sum_{i=1}^{n} \hat{c}i = \sum{i=1}^{n} c_i + \Phi(D_n) - \Phi(D_0)i=1∑nc^i=i=1∑nci+Φ(Dn)−Φ(D0)

只要 Φ(Di)≥Φ(D0)\Phi(D_i) \geq \Phi(D_0)Φ(Di)≥Φ(D0) 对所有 iii 成立(通常取 Φ(D0)=0\Phi(D_0) = 0Φ(D0)=0 且 Φ(Di)≥0\Phi(D_i) \geq 0Φ(Di)≥0),总摊还代价就是总实际代价的上界。

2 倍扩容的例子

定义势函数 Φ(Di)=2⋅num−size\Phi(D_i) = 2 \cdot \text{num} - \text{size}Φ(Di)=2⋅num−size,其中 num 是当前元素个数,size 是当前表容量。

  • 普通插入(未扩容):ci=1c_i = 1ci=1,num 增 1 而 size 不变,ΔΦ=2\Delta\Phi = 2ΔΦ=2。摊还代价 c^i=1+2=3\hat{c}_i = 1 + 2 = 3c^i=1+2=3。
  • 扩容插入:ci=1+old_sizec_i = 1 + \text{old\_size}ci=1+old_size,num 增 1 而 size 翻倍。扩容前 num = size,扩容后 Φ\PhiΦ 从 2⋅size−size=size2 \cdot \text{size} - \text{size} = \text{size}2⋅size−size=size 变为 2(size+1)−2⋅size=22(\text{size}+1) - 2\cdot\text{size} = 22(size+1)−2⋅size=2。ΔΦ=2−size\Delta\Phi = 2 - \text{size}ΔΦ=2−size。摊还代价 c^i=(1+size)+(2−size)=3\hat{c}_i = (1 + \text{size}) + (2 - \text{size}) = 3c^i=(1+size)+(2−size)=3。

两种情况下摊还代价都是 3。nnn 次插入总代价 ≤3n=O(n)\leq 3n = O(n)≤3n=O(n)。

1.5 倍扩容(ArrayList)

类似地定义势函数后推导,每次操作的摊还代价为 4,nnn 次操作总代价为 O(n)O(n)O(n)。

三种方法对比

方法 思路 特点
聚合法 先求总代价,再平均 最直观,但不同操作不能有不同的摊还代价
核算法 给每种操作定价,多收的存起来 灵活,但需要证明存款始终 ≥ 0
势能法 定义势函数,用势差抵消代价 最形式化,势能作用于整个数据结构而非特定对象

三种方法得到的结论相同,只是分析角度不同。聚合法最简单,势能法最精确也最通用。

相关推荐
胡图图不糊涂^_^2 小时前
网络原理笔记
java·网络·笔记·学习·tcp/ip·http·https
嘻嘻哈哈樱桃2 小时前
牛客经典101题题解集--哈希
java·数据结构·python·算法·leetcode·职场和发展·哈希算法
三品吉他手会点灯2 小时前
STM32 VSCode 开发-与Keil MDK协同开发环境搭建
笔记·vscode·stm32·单片机·嵌入式硬件
自我意识的多元宇宙2 小时前
【数据结构】 红黑树
数据结构·算法
wayz112 小时前
Day 15 编程实战:KMeans聚类与股票风格分类
算法·机器学习·分类·kmeans·聚类
不知名的老吴2 小时前
数据结构与算法之排序算法
算法·排序算法
Brilliantwxx2 小时前
【算法题】日期类算法题
开发语言·c++·笔记·程序人生·算法
不会编程的懒洋洋2 小时前
C# IDisposable 和 using
开发语言·笔记·机器学习·c#·.net·visual studio·c#基础
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.27):检查网格中是否存在有效路径
算法·leetcode·职场和发展