(b)可扩充向量

官官方信息技术老师·14 页·深入(追求细节与边界)·0 次浏览·38 天前
动态扩容均摊分析数据结构

可扩充向量

看完你能讲清可扩充向量如何搬家扩容,以及代价怎么摊平

按 空格/→ 演示下一步

问一句
1 / 14 页

全部页面点击任意一页,跳回舞台从这页播放

动态扩容均摊分析数据结构

可扩充向量

看完你能讲清可扩充向量如何搬家扩容,以及代价怎么摊平

1第 1 页 · 可扩充向量

静态空间的困境

上一页我们说向量要在内存里连续存放。可问题来了:建向量时得先向系统要一块地,要多大呢?要小了数据装不下,要大了平时又白白空着——这个两难,就是本页的主角。

容量在创建时定死
静态分配的空间大小固定,之后无法更改
上溢
元素数超过容量时,静态结构直接无地可放
规模无法预知
实际使用中数据量往往事先不知道,预留两难
预留的浪费
按最大可能预留,多数时间大量空间闲置
固定座位的停车场对应 →静态容量

车位建好就定死,车多了进不来,车少了空着

2第 2 页 · 静态空间的困境

动态空间管理

上页看到静态空间一满就束手无策。生活中房子住不下怎么办?换一套更大的,把家具搬过去,再退掉旧房。向量遇到容量不足时,用的正是这套「换房」策略。

申请更大空间
分配一块更大的新数组(常见为原容量的两倍)
搬迁数据
把旧数组元素逐个复制到新空间,代价 O(n)
释放旧空间
搬迁完成后归还旧内存,避免泄漏
迭代器失效
旧地址不再有效,搬迁后原有指针引用作废
搬家换大房子对应 →向量扩容

换大房对应申请新空间,搬家具对应复制元素,退租对应释放旧空间

3第 3 页 · 动态空间管理

扩容的完整流程

空间不够时,向量按四步完成一次扩容,每步各司其职。

1
发现已满
下次插入前检测 size == capacity,提前触发扩容
2
申请加倍
开辟一块约 2 倍容量新空间,均摊成本更低
3
搬移数据
把旧元素逐一复制或移动到新空间,O(n) 开销
4
释放旧区
归还旧空间给系统,避免内存泄漏,更新指针
4第 4 页 · 扩容的完整流程

递增式扩容

上一页我们看到扩容流程已打通,但还有一个关键决策没定:每次扩多少?搬家太频繁会累,一次搬太大又浪费——增量策略就是在这两者间找平衡。

固定增量
每次扩容追加固定容量,如每次多10个槽位
均摊开销低
单次扩容成本被平摊到多次追加操作上
增幅保守
扩幅小于容量翻倍策略,空间利用率更高
给水壶续水对应 →递增式扩容

每次固定加半杯水对应每次追加固定增量,够用再续

新容量=旧容量+Δ,均摊成本=k⋅Δ+∑搬迁n\text{新容量}=\text{旧容量}+\Delta,\quad \text{均摊成本}=\frac{k\cdot\Delta+\sum\text{搬迁}}{n}
5第 5 页 · 递增式扩容

加倍式扩容

上一页的递增式扩容每次只多要一点,扩得慢。想想搬家:与其一趟趟搬一个箱子,不如直接换套大一倍的房子——容量翻倍,次数骤减,这就是加倍式扩容。

容量翻倍
每次扩容新空间为旧容量的两倍
次数对数级
n 次插入只需约 log n 次扩容
均摊低开销
总搬家成本摊到每次插入,近似常数
换更大的房子对应 →加倍式扩容

房子翻倍对应容量翻倍,搬家次数对应扩容次数

2m≥n⇒m=O(log⁡n)2^m \ge n \Rightarrow m = O(\log n)
6第 6 页 · 加倍式扩容

递增 vs 加倍

两者都能自动扩容,但扩多少差一个数量级——这直接决定扩容次数与摊还代价。

递增式扩容
  • 每次固定加常数 c,如 +10
  • n 次插入约扩容 n/c 次
  • 总搬移代价 O(n²)
加倍式扩容
  • 每次容量翻倍,如 ×2
  • n 次插入约扩容 log n 次
  • 总搬移代价 O(n)
几乎总是选加倍:摊还后单次插入仅 O(1);递增式只在插入极少、内存极紧时才划算。
7第 7 页 · 递增 vs 加倍

扩容代码实现

cpp

一段真实的 C++ expand 实现,看扩容的判断、加倍与搬迁如何落地。

代码高亮加载中…

先判断是否真满(多余调用零开销),容量按加倍翻番,再逐元素复制到新空间,最后释放旧区——正是加倍式扩容流程的代码化。

8第 8 页 · 扩容代码实现

为什么不能只看最坏情况

上一页我们看到加倍式扩容看起来最划算。但如果按单次最坏情况算,一次扩容要搬动全部 n 个元素,成本高得吓人——问题出在衡量方式上。

单次最坏的误导
某一次扩容恰好搬 n 个元素,但这种情况极少发生
偶尔 vs 频繁
扩容是低频事件,用最坏情况给每次操作定罪过于悲观
摊还分析
把偶尔的高成本摊到之前的多次操作上,每次平均只需 O(1)
n 次插入 n 次搬运
连续插入 n 个元素总搬运次数约 2n,均摊每次 O(1)
办年卡对应 →摊还分析

单次入场最坏要花全款,但摊到每次入场后近乎免费

2nn=O(1)\frac{2n}{n}=O(1)
9第 9 页 · 为什么不能只看最坏情况

分摊复杂度

上一页我们看到:只盯单次扩容的最坏代价,会得出「动态数组很慢」的错误结论。更公平的算法是把罕见的大开销摊到之前那些廉价操作上,按平均代价来评估。

分摊分析
把偶尔的高成本摊到多次操作上,算平均代价
总代价法
连续 n 次操作总耗时,除以 n 得单次均值
与均摊区别
不是概率平均,而是任何输入序列都成立的保证
请客吃饭 AA 结账对应 →分摊复杂度

一次大额总账平摊到每个人,单人均价就变小了

T(n)=O(n) ⇒ 单次分摊=T(n)/n=O(1)T(n)=O(n)\ \Rightarrow\ \text{单次分摊}=T(n)/n=O(1)
10第 10 页 · 分摊复杂度

两种策略的分摊分析

从 n 次插入出发,沿两条分支看每次扩容搬多少元素,最后对比总代价与均摊复杂度。

图解渲染中…
b3:递增式:每轮扩容搬迁代价累加,总计约 1+2+…+nc3:加倍式:搬迁代价 1,2,4,8… 成倍增长但求和被末项主导
11第 11 页 · 两种策略的分摊分析

自测:选对策略

点击作答

向量采用加倍式扩容,连续追加 n 个元素,为何总耗时是 O(n) 而不是 O(n²)?

12第 12 页 · 自测:选对策略

要点收束

  • ✓静态容量不足时需动态扩容,容量按需增长
  • ✓递增式省内存但摊还 O(√n),加倍式 O(1)
  • ✓扩容虽有开销,摊还后单次操作均摊代价很低
  • ✓摊还分析看长期平均,不被单次最坏迷惑
  • ✓选策略先问:内存敏感还是操作吞吐优先
延伸主题:动态缩容:装填因子低于阈值时收缩上界增长率:倍增扩容的容量上界分析分摊复杂度三种证明:聚合/记账/势能
13第 13 页 · 要点收束

课后思考

先独立想几分钟,再对照参考答案——开放问题没有唯一解。

1加倍式扩容的分摊复杂度是 O(1),那单次扩容瞬间会发生什么?为什么均摊后看不到了?

参考答案扩容瞬间确实要整体搬迁,是 Θ(n) 的昂贵操作。分摊分析把这次开销摊到之前 n 次廉价插入上,平均每次仍是常数——不等于单次变快,而是长期总账变好。

2如果内存紧张、又知道数据规模大致上限,你会怎么调整扩容策略?

参考答案可先用 reserve 预分配接近上限的容量,避免反复搬迁;必须扩容时也可选较小的增长因子,用稍高的分摊代价换取更少的空闲空间——按场景权衡。

3扩容后旧数组元素全部搬到新地址,程序里已拿到的指针或迭代器会怎样?

参考答案全部失效——它们指向旧内存,扩容后继续使用是未定义行为。这也是许多语言文档强调'扩容后迭代器失效'的原因,持有引用前要想清楚生命周期。

14第 14 页 · 课后思考