惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
N
News and Events Feed by Topic
T
Troy Hunt's Blog
Help Net Security
Help Net Security
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
M
MIT News - Artificial intelligence
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V2EX - 技术
V2EX - 技术
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
大猫的无限游戏
大猫的无限游戏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
Cisco Talos Blog
Cisco Talos Blog
T
Threatpost
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
SegmentFault 最新的问题
I
InfoQ
H
Hacker News: Front Page
D
Docker
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Blog — PlanetScale
Blog — PlanetScale
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
Know Your Adversary
Know Your Adversary
博客园 - 【当耐特】
T
Tor Project blog
U
Unit 42
H
Heimdal Security Blog
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy & Cybersecurity Law Blog
PCI Perspectives
PCI Perspectives
美团技术团队
O
OpenAI News
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog
GbyAI
GbyAI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
MyScale Blog
MyScale Blog

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
复杂度的均摊分析法
Ofnoname · 2026-04-25 · via 博客园_首页

动态数组扩容问题是均摊复杂度分析最经典的应用:

  • 动态数组的尾插 push_back,有时会触发扩容;
  • 一旦扩容,就要申请更大的内存、搬运旧元素、再插入新元素。某一次操作的代价完全可能是 \(O(n)\)
  • 但是,动态数组尾插的复杂度是均摊 \(O(1)\)

类似的现象其实非常多:单看某一次操作,它们都可能很贵;但把它们放到足够长的操作序列里,平均到每一步,复杂度却仍然很低。均摊分析研究的正是这种现象。

平均情况分析 vs 均摊分析

均摊分析和平均情况分析(average-case analysis)不是同一个概念。

平均情况分析通常要假设输入服从某种概率分布,然后计算期望成本。比如快速排序的平均复杂度分析,核心就在于对输入或主元分布做假设。例如,快速排序平均是 \(O(n\log n)\) 的,但最坏可能是平方复杂度。

均摊分析则不同。它不依赖概率和输入,一个 vector 无论怎样尾插,均摊复杂度也是 \(O(1)\)

我们可以定义:对于一个数据结构的一段操作序列

\[\sigma = (o_1,o_2,\dots,o_m), \]

如果总实际成本是

\[C(\sigma)=\sum_{i=1}^{m} c_i, \]

能否证明存在某个函数 \(f(m)\),使得

\[C(\sigma)\le m\cdot f(m)。 \]

如果可以,那么就说这类操作的均摊复杂度是 \(f(m)\)。可以理解为,均摊分析给出的是“整段序列的总账不贵”的承诺。于是,均摊 \(O(1)\) 并不意味着每次操作都只花常数时间,它真正的含义是:在任意长的合法操作序列里,总成本相比操作次数的复杂度没有那么高。

经典例子:动态数组

动态数组是本文最好的主线。假设一个动态数组有两个状态量:

  • size:当前元素个数
  • capacity:当前容量

执行 push_back(x) 时:

  • size < capacity,直接写入,成本记为 \(1\)
  • size = capacity,则申请一个两倍大的新数组,把旧元素全部搬过去,再插入新元素。若旧容量为 \(k\),那么这次成本记为 \(k+1\)

单次最坏显然是线性的;但长期看却是均摊常数。下面分别用三种方法来理解它。

聚合法:直接算前 \(n\) 次操作的总成本

聚合法直接计算多步的总成本。由于每次扩容是两倍,数组容量会按 \(1,2,4,8,\dots\) 这样翻倍增长。做完前 \(n\) 次插入时,普通写入本身一共发生了 \(n\) 次,成本是 \(n\)。除此之外,还会发生若干次扩容搬运,搬运量依次是 \( 1+2+4+\cdots+2^{k-1}, \),其中 \(2^{k-1}<n\le 2^k\) 。于是

\[1+2+4+\cdots+2^{k-1}<2^k\le 2n。 \]

所以总成本满足

\[C(n)\le n + 2n = 3n。 \]

也就是说,前 \(n\)push_back 的总成本是 \(O(n)\),因此平均到每次操作,复杂度就是 \(O(1)\)

这个证明非常重要,因为它第一次把“偶尔很贵”和“长期很便宜”这两个看似矛盾的结论接起来了。扩容确实贵,但扩容发生得足够稀疏,所以总账仍然可控。

记账法:平时多收一点,留着给扩容买单

记账法比聚合法更有“预算”的味道。我们不直接算总账,而是假装给每次操作都规定一个均摊收费标准。

还是看动态数组尾插。设每次 push_back 都收费常数代价 \(3\)

  • 如果这次没有扩容,真实成本只有 \(1\),那就剩下 \(2\) 存起来;
  • 如果这次触发扩容,就从之前存下来的余额里支付搬运成本。

为什么这个方案可行?考虑一次从容量 \(m\) 扩到 \(2m\) 的扩容。在这次扩容发生前,数组已经经历了从半满到装满的一系列普通插入。也就是说,每次扩容时,前面都有发生了足够多(大约 \(m/2\) )的便宜插入,每次都能攒出足够余额,总共可以积累出足以覆盖这次搬运的资金。

势能法

从记账的思想更进一步,我们就可以引出势能法。它的思想是:给数据结构状态 \(D_i\) 定义一个势能函数 \(\Phi(D_i)\),然后把第 \(i\) 次操作的均摊成本定义为

\[\hat c_i = c_i + \Phi(D_i)-\Phi(D_{i-1})。 \]

这里:

  • \(c_i\) 是第 \(i\) 次操作的真实成本;
  • \(\hat c_i\) 是我们定义出来的均摊成本;
  • \(\Phi(D_i)-\Phi(D_{i-1})\) 表示状态势能的变化。

这样一次操作的均摊成本就是他的"实际成本 + 势能变化"

如果我们能选取出合适的势能函数表示当前状态(要保证势能始终非负,并且初始势能为 \(0\)),那么对前 \(m\) 次操作有

\[\sum_{i=1}^{m} \hat c_i = \sum_{i=1}^{m} c_i + \Phi(D_m)-\Phi(D_0) \ge \sum_{i=1}^{m} c_i。 \]

所以,只要每一步的均摊成本都有常数上界,那么真实总代价也就有同阶上界。

对动态数组,一个经典势能函数是

\[\Phi = 2\cdot \text{size} - \text{capacity}。 \]

这个定义的直觉是:数组越接近装满,也就越接近下一次扩容,势能越大。

对于不扩容的普通插入。真实成本 1,势能变化 2(因为 size 增加 1,而 capacity 不变),均摊代价 3

对于触发扩容的插入。插入前 size = mcapacity = m,势能是 \(\Phi_{\text{old}}=m\);插入后 size = m+1capacity = 2m,势能变成

\[\Phi_{\text{new}} = 2(m+1)-2m=2。 \]

这次真实成本是搬运 \(m\) 个旧元素再插入新元素(共 \(m + 1\)),势能变化是 \( \Delta \Phi = 2-m。 \)。所以均摊成本为 \(3\)

不扩容时是 \(3\),扩容时还是 \(3\)。因此动态数组尾插的均摊复杂度就是 \(O(1)\)

f120d24b-1fb1-4272-8512-320fc42faeb1

其他常见例子

栈的 MULTIPOP

考虑一个支持三种操作的栈:

  • PUSH(x):压栈,成本为 \(1\)
  • POP():弹栈,成本为 \(1\)
  • MULTIPOP(k):连续弹出最多 \(k\) 个元素,成本等于实际弹出的元素数。

单看一次 MULTIPOP(k),它当然可能要弹很多元素,代价不是常数。但如果看一整段操作序列,总弹出次数其实不可能超过总压栈次数。因为每个元素最多只会被压入一次、弹出一次。

因此,如果总共有 \(m\) 次操作,那么所有弹栈动作加起来不会超过线性级别,于是总成本是 \(O(m)\),均摊每次仍是 \(O(1)\)

这个例子特别适合作为均摊分析的第一个训练:只要能发现“每个元素最多被贵处理一次”,总成本往往就很好控制。

二进制计数器的自增

再看一个更经典的例子。一个二进制计数器不断执行 INCREMENT,一次操作的成本定义为被翻转的位数。

例如:

  • 0111 \to 1000 的成本是 \(4\)
  • 1010 \to 1011 的成本是 \(1\)

单次最坏显然不是常数,但连续执行 \(n\) 次自增时,第 \(0\) 位每次都翻,第 \(1\) 位每两次翻一次,第 \(2\) 位每四次翻一次,一般地,第 \(i\) 位每 \(2^i\) 次翻一次。

所以总翻转次数满足

\[n + \left\lfloor \frac{n}{2} \right\rfloor + \left\lfloor \frac{n}{4} \right\rfloor + \cdots < 2n。 \]

因此,\(n\) 次自增的总成本是 \(O(n)\),均摊下来每次仍是 \(O(1)\)。这个例子也很有代表性:某次昂贵进位之所以会发生,是建立在之前很多较低位状态积累的基础上;他发生的足够少,所以总成本并不会爆炸。

另一个视角是,规定每次 INCREMENT 收费 \(2\)。当某一位从 0 变成 1 时,真实成本是 \(1\),剩下的 \(1\) 单位信用就附着在这个新的 1 上。以后当它从 1 变回 0 时,就用这份信用支付那次翻转。

于是每个 1 的“出生”和“死亡”在它变成 1 的那一刻就已经付清了。虽然某次自增可能会消灭一长串连续的 1,但这些 1 各自都携带了自己的预算,所以集中翻转时并不会额外欠债。

单调队列

单调队列(monotonic queue)是滑动窗口问题里的常用工具。以滑动窗口最大值为例:给定数组 \(a_1,a_2,\dots,a_n\) 和窗口大小 \(k\),要求每个长度为 \(k\) 的窗口最大值。

标准做法是维护一个双端队列,队列里存下标,并保持这些下标对应的值单调递减。处理位置 \(i\) 时,通常做三件事:

  • 把队首已经滑出窗口的下标弹出;
  • 当队尾对应的值小于等于 \(a_i\) 时,不断从队尾弹出;
  • 把当前下标 \(i\) 压入队尾。

看起来最可疑的是第二步。一次插入新元素时,完全可能触发很多次队尾弹出。比如遇到一个很大的新元素,它会把队尾一串更小的元素全部清掉。单次看,这一步显然不是常数。

但从聚合法的角度看,整个算法仍然是线性的。因为:

  • 每个元素在被扫描到时,只会压入队尾一次;
  • 它之后要么因为滑出窗口从队首弹出,么因为遇到更大的元素从队尾弹出;不管是哪一种,被弹出以后它都不会再回来。

因此,在整个过程中,所有入队次数总共是 \(n\),所有出队次数总共也是 \(O(n)\)。虽然某一步的 while 循环可能连续弹很多次,但把所有这些弹出加起来,最多不过线性数量。

看到循环里面还有循环,很多人会本能地把它看成平方复杂度;但只要循环内部“消耗掉的对象”一旦离开就不再回来,均摊分析往往能把复杂度压回线性。

两个栈实现队列

队列可以用两个栈来实现。考虑用两个栈 S_inS_out 实现队列:

  • enqueue(x):把元素压入 S_in
  • dequeue():如果 S_out 非空,直接弹出;如果 S_out 为空,就把 S_in 中所有元素逐个倒入 S_out,再弹出队首。

某次 dequeue() 看上去可能很贵,因为它会在那一刻做整批搬运。但如果从元素的一生看,这个结构其实非常简单。

一个元素从入队到出队,最多只会经历四个基本动作:

  1. 压入 S_in
  2. S_in 弹出
  3. 压入 S_out
  4. S_out 弹出

于是我们可以规定:每个元素在 enqueue 时统一收取常数费用 \(4\),分别用于支付它未来的一生。这样一来,后面即使某一次 dequeue() 触发了整批倒栈,那也不过是在集中兑现许多元素早已缴纳好的费用而已。因此,入队和出队的均摊复杂度都是 \(O(1)\)

从势能的角度看,可以定义势能为

\[\Phi = 2|S_{\text{in}}|。 \]

这个定义的直觉是:S_in 里的每个元素将来都至少还要经历两次动作——从 S_in 弹出,再压入 S_out。所以它们的存在本身就意味着未来还藏着一些尚未支付的成本。对一次 enqueue(x) 来说,真实成本是 \(1\),势能增加 \(2\),于是均摊成本为 \(3\)

对一次不触发倒栈的 dequeue(),真实成本是 \(1\),势能不变,因此均摊成本是 \(1\)

对一次触发倒栈的 dequeue(),设此时 S_in 中有 \(k\) 个元素。真实成本是把这 \(k\) 个元素逐个倒到 S_out\(2k\),再弹出队首的 \(1\),总共 \( 2k+1。 \)。而与此同时,S_in 变空,势能减少 \( \Delta \Phi = -2k。 \)。均摊成本为 \( 1 \)

哈希表扩容

哈希表(hash table)里的扩容和动态数组非常相似。平时插入一个键值对,通常只需常数时间;但一旦装载因子过高,就可能触发 rehash,把整张表里的元素重新分布到更大的桶数组里。

单次 rehash 的最坏代价显然是线性的,但它不会频繁发生。只要扩容策略是按常数倍增长,那么在一长串插入操作里,总搬运量仍然是线性的,推导过程和变长数组完全相同。因此插入的均摊复杂度仍为 \(O(1)\)

动态数组扩展

前面一直只讨论 push_back,但实际动态数组通常还会支持 pop_back。这时问题会变得更微妙一些:如果我们在数组刚满时就扩容、刚删掉一个元素就立刻缩容,那么在边界附近反复执行 push_backpop_back,就可能出现“来回抖动(thrashing)”:次扩完马上缩,一次缩完马上又扩,这一串合法操作每次都是 \(O(n)\) 的,复杂度就炸了。

解决办法是把扩容阈值和缩容阈值分开:例如满了才扩到 2 倍,而只有当 size 下降到 capacity / 4 时才缩到 capacity / 2。这样就留出了一个缓冲区,避免在同一条边界附近频繁震荡;进一步做势能分析或记账分析,仍然可以证明 push_back / pop_back 的均摊成本是 \(O(1)\)

另外,虽然我们都用"2 倍扩容"举例子,但可以证明,只要容量按常数倍增长,即从 \(m\) 扩到 \(\alpha m\),其中 \(\alpha > 1\) 是常数,那么总搬运成本仍然是一个等比级数:

\[1 + \alpha + \alpha^2 + \cdots + \alpha^k = O(\alpha^k), \]

因此均摊插入成本依然是 \(O(1)\);相反,如果每次只固定增加一个常数,比如每次只多开 100 个位置,那么扩容会发生得越来越频繁,总搬运成本会累积成平方级,总体就不再是均摊常数。工程上通常选 2 倍扩容,主要是因为它在“减少扩容次数”和“控制空闲空间浪费”之间给出了一个很均衡的折中:倍数太小,会导致扩容过于频繁;倍数太大,虽然扩容次数更少,但会浪费更多未使用容量。

更高难度数据结构的均摊分析例子

很多真正强力的数据结构,复杂度结论本身就建立在均摊分析上。这里只列举,不再细证。

并查集

带路径压缩(path compression)和按秩合并(union by rank)的并查集,是均摊分析最经典的高级案例之一。

单次 find(x) 可能要沿着父指针一直爬到根,看起来并不便宜;但路径压缩会在返回过程中把整条路径上的节点直接挂到根附近。于是,那些这次被访问到的节点,以后再查找时就会更快。

最终可以证明:对 \(n\) 个元素执行 \(m\) 次操作,总成本是

\[O(m\,\alpha(n)), \]

其中 \(\alpha(n)\) 是反阿克曼函数(inverse Ackermann function),增长极慢,在实际规模下几乎可以视为常数。

这个结论本质上就是均摊分析:某次 find 可能很贵,但那次昂贵访问顺便把未来很多次查找都优化掉了。

伸展树

Splay Tree 的每次访问都会通过一系列旋转把目标节点伸展到根。单次访问的最坏复杂度并不漂亮,但整个访问序列的总代价却很优雅。

经典分析会定义某种与子树规模相关的势能,例如

\[\Phi(T)=\sum_{x\in T}\log s(x), \]

其中 \(s(x)\) 是以节点 \(x\) 为根的子树大小。通过一套势能分析,可以得到访问的均摊复杂度为 \(O(\log n)\)。这个例子说明,势能法的用途远不止“为扩容攒点钱”那么简单。它还可以用来衡量一棵树当前有多“失衡”,而单次昂贵的重构恰恰是在降低这种失衡。

斐波那契堆

Fibonacci Heap 是另一个典型案例。它的一些操作,比如 insertmelddecrease-key,都能达到非常低的均摊复杂度,而代价被集中到少数整理操作上。只要一个数据结构允许“延迟维护(lazy maintenance)”,均摊分析几乎就会自然出现。