在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

动态数组

动态数组在连续内存中保存元素,区分实际元素数 size 和可容纳数量 capacity。按索引访问只需计算 address + index × element_size,所以读写为 O(1)。连续布局的成本体现在插入、删除和扩容时。

可以把它看成右侧留有空位的一排编号格子。追加先使用第一个空位;没有空位时,申请两倍长度的新数组,搬走全部值,再释放旧数组。少数昂贵的扩容支持多数便宜的追加。

动态数组如何工作

底层数组有 capacity 个位置,前 size 个有效,其余备用。本课删除后不自动缩容。

  • 索引访问:get、set 直接算地址,不经过邻居。索引 0 与 9,000 都是同类计算。
  • 追加:先检查 size == capacity;有空位就写入索引 size,然后增加 size。
  • 扩容:满时申请 2 × capacity,逐个复制 size 个元素,释放旧数组,最后追加。单次复制为 O(n),但几何增长使多次追加的总成本保持线性,每次摊还 O(1)。
  • 移位:索引 i 插入时,把从 i 开始的 size − i 个元素右移;删除时把后面的 size − i − 1 个元素左移。最坏 O(n),头部最贵,尾部无需移位。

插入必须从右向左复制,先把末尾搬到新位置,避免覆盖还没搬走的值。删除反过来,从左向右填补空位。

逐步演示

默认空数组 capacity = 4,运行九项操作:

  1. append(5):写索引 0,size = 1。
  2. append(8):写索引 1,size = 2。
  3. append(2):写索引 2,size = 3。
  4. append(9):写索引 3,size == capacity == 4,数组已满。
  5. append(4):先扩为容量 8,依次复制 5、8、2、9,释放旧数组,再把 4 写入索引 4。结果 [5, 8, 2, 9, 4],size = 5、capacity = 8。这次追加包含四次复制。
  6. get(2):直接读取索引 2,得到 2,不扫描。
  7. set(2, 6):直接改写为 6,得到 [5, 8, 6, 9, 4]。
  8. insert(1, 7):按从后向前的顺序,把 4 移到索引 5、9 到 4、6 到 3、8 到 2,再把 7 写入 1。得到 [5, 7, 8, 6, 9, 4],size = 6。一次插入移动四个元素。
  9. remove(3):移除值 6,把 9 从 4 移到 3、4 从 5 移到 4,size 减为 5。结果 [5, 7, 8, 9, 4],最后一个占用位置恢复备用。

结束时容量 8、元素 5,剩余三个空位。画布右侧虚线格表示备用容量,实线格下方的括号表示 size。

时间与空间复杂度

操作时间原因
get(i) / set(i, v)O(1)按索引算地址
append(v)摊还 O(1),扩容时 O(n)通常一次写入,偶尔复制到两倍数组
insert(i, v)O(n)右移 size − i 个元素
remove(i)O(n)左移 size − i − 1 个元素

存储占 O(capacity)。只追加、按倍数增长时,它是元素数的 O(n),扩容刚完成时约有一半空位。由于本课不缩容,大量删除后的已分配空间取决于历史最大容量,不能再只用当前 size 估计。

本课每次扩大两倍。实现也可选择其他大于 1 的常数因子,仍可得到摊还 O(1) 追加。常见实现示例包括 Java ArrayList 约 1.5 倍、C++ std::vector 常见 1.5–2 倍、CPython list 约 1.125 倍;具体策略随实现和版本而异,不是接口承诺。

边界情况

  • 尾部插入或删除:insert(size, v) 等同追加,无移位,但满时仍要扩容;remove(size − 1) 只减小 size,为 O(1)。
  • 头部插入或删除:insert(0, v) 移动全部原元素,remove(0) 移动全部剩余元素。这是最坏情况;链表的头插则为 O(1)。
  • 索引越界:get、set、remove 要求 0 ≤ i < size;insert 允许 0 ≤ i ≤ size。否则抛出 IndexError;输入面板会禁用这类索引的按钮,动画不会执行。
  • 空数组:get、set、remove 没有有效索引;insert(0, v) 有效。
  • 显示上限:画布在容量 16 停止翻倍,实际结构没有这个固定上限,受内存或实现限制。

常见错误

  • 把每次追加都看作 O(1)。 触发扩容的单次追加复制全部元素。O(1) 是多次操作的摊还成本。
  • 移位方向颠倒。 插入从低索引往高复制,会覆盖待移动的值,把同一值传播到后面。插入从高到低,删除从空位向后。
  • 删除后留洞。 动态数组的有效区连续,不是在 i 留下 null。后面的元素必须补位。
  • 混淆 size 和 capacity。 有备用位置不代表能读取;有效索引仍止于 size − 1。
  • 忽略工作负载。 动态数组擅长索引访问与顺序扫描,头部插入和删除较贵。按主要操作选择数组或链表。