SWE · Coding

Meta 动态数组Append:单次最坏与摊还复杂度

用几何扩容模型解释没有预分配的append为何仍能实现总线性时间。

题目重点:用几何扩容模型解释没有预分配的append为何仍能实现总线性时间。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 教学实现使用容量不足时翻倍的动态数组,按槽位复制计成本;不声称某语言运行时严格翻倍。存储的是对象引用,复制引用不等于深复制对象。
  • 只分析append构造N个元素,不把任意位置插入、昂贵对象创建或内存分配失败也算作常数。

解题路线

  1. 未触发扩容时写一个槽位O(1);容量满时分配新数组并复制当前所有引用,本次最坏O(N)。单次最坏与连续操作的平均成本是两个不同问题。
  2. 容量1、2、4、8依次增长时,总复制量为1+2+4+…+最后旧容量。几何级数小于2N,加上N次新元素写入,总槽位工作O(N),故每次append摊还O(1)。
  3. 分配空槽位也需要计成本,几何增长使历次新容量之和仍O(N)。本例copied只计旧元素复制,并不测墙钟速度;验证指标不能偷换为完整运行时间。
  4. 若每次只增加固定常数容量,则复制量为线性增长数列,总成本Θ(N²)。关键是几何增长策略,不能凭“动态数组”名称就断言摊还常数。
  5. 预分配能减少重分配和峰值延迟,但需知道或估计N,可能浪费内存。在线输入长度未知可采用动态增长;对严格延迟要求要考虑分块结构或提前reserve的接口。

手动走一遍

追加9个元素时容量经历1/2/4/8/16,旧元素复制0+1+2+4+8=15,小于2×9。第9次本身复制8个元素,不是最坏O(1),但9次总体仍线性。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

class DoublingArray:
    """Teaching model, not a claim about a language runtime's growth factor."""
    def __init__(self):
        self.storage=[];self.size=0;self.copied=0
    def append(self,value):
        if self.size==len(self.storage):
            capacity=max(1,2*len(self.storage))
            grown=[None]*capacity
            for i in range(self.size):grown[i]=self.storage[i]
            self.copied+=self.size;self.storage=grown
        self.storage[self.size]=value;self.size+=1
    def values(self):
        return self.storage[:self.size]

复杂度

N次append总时间O(N),单次最坏O(N)、摊还O(1)。稳定存储O(N),扩容期间旧数组和新数组同时存在,峰值仍O(N)但常数增加。values复制当前元素需O(N)时间与空间。

易错点与追问

容易失分的边界

  • 把摊还O(1)说成每次最坏O(1)。
  • 忽略容量策略,或把引用复制当任意大小对象深复制。
  • 用copied计数声称实际分配器延迟已被测量。

继续深挖

  • 如果增长因子是1+ε,复制次数与空闲容量之间如何权衡?
  • 严格实时场景如何限制每次迁移量,并保证迭代器与引用稳定性?
✓已复制微信号!