几种list的复杂度 & 内存 & 扩容

61 阅读5分钟

一、复杂度 × 内存 × 扩容总表(n 为元素个数)

容器get(i)/set(i)末尾 add/remove头部 add/remove中间插入/删除(已知索引)contains/遍历额外内存开销扩容/收缩策略典型场景
ArrayListO(1)摊还 O(1) (偶发 O(n))O(n)O(n)(搬移)O(n)低:一个 Object[] + 少量空槽按比例扩容(约 1.5×) ;默认不自动收缩,可 trimToSize()绝大多数业务列表、排序/二分、随机访问
LinkedListO(n)O(1)(有尾指针)O(1)定位 O(n),定位后 O(1)O(n)高:每元素一 Node(prev/next 指针 + 对象头)无“扩容”,每次插入分配新 Node;释放即内存可回收仅当已持有节点/迭代器且频繁插删
ArrayDeque不支持下标随机访问O(1)O(1)不支持中间插入遍历 O(n)低:环形 Object[]容量 2 的幂,满时 ×2;通常不自动收缩队列/栈/双端队列(优于 LinkedList)
CopyOnWriteArrayListO(1)O(n) (写时复制)O(n)O(n)读遍历 O(n)(无锁),迭代弱一致中:底层数组 + 写时复制的暂态数组每次写产生新数组(约 1.5× 拷贝) ,无自动收缩读多写少、监听器列表、事件分发

速记:随机访问/遍历 → ArrayList;双端队列 → ArrayDeque;已定位节点附近频繁插删才考虑 LinkedList;读多写少的观察者列表 → CopyOnWriteArrayList。****


二、逐个讲清

1) ArrayList(动态数组)

  • 内存模型:一段连续的 Object[],size ≤ capacity,未用槽位是 null 引用;引用大小与平台相关(常见 4B/8B)。

  • 扩容:不足时 按比例扩容(常见 ≈1.5×) → Arrays.copyOf 整体搬移到更大的数组;因此尾插是摊还 O(1)

  • 收缩:默认不主动收缩(避免抖动与频繁拷贝),可在需要时 trimToSize() 或重新构造。

  • 性能特征

    • CPU 缓存友好(连续内存),遍历/随机访问很快;
    • 中间插删要搬移,O(n) ;大量删除时建议倒序或 removeIf。
  • 实战建议:批量插入前 ensureCapacity(expected) 降低多次扩容成本。

2) LinkedList(双向链表)

  • 内存模型:每个元素对应一个 Node{prev, item, next} 对象,不连续;每个节点多出 2 个指针与对象头,常数开销大,GC 更频繁。

  • 复杂度:已定位到节点后插删 O(1) ,但定位本身 O(n) ;get(i) 需要从近端线性走。

  • 适用面:仅在已持有迭代器/节点且在其附近高频插删、或头尾操作很多的少数场景。

  • 对比:做队列/栈,ArrayDeque 往往更快更省内存。

3) ArrayDeque(环形数组的双端队列)

  • 内存模型:一个容量为 2 的幂 的 Object[],用 head/tail 下标环形前进(按位与掩码取模),空槽为 null。

  • 扩容/收缩:满时容量 ×2 并重排;通常不自动收缩(避免震荡),可在“长期变小”的场景手动重建。

  • 性能特征:头/尾入出都是 O(1) ;没有中间插入语义,也没有下标随机访问。

  • 选型队列/栈/双端队列首选,绝大多数情况下优于 LinkedList。

4) CopyOnWriteArrayList(写时复制数组)

  • 内存模型:内部是数组;每次写都会复制出一个新数组并替换旧引用,读侧看到的是快照
  • 复杂度:读遍历无锁但写是 O(n) ,且会产生短暂两份数组的内存占用。
  • 适用面读远多于写(例如监听器/订阅者列表、系统配置快照);写入频繁或列表很大时不合适。
  • 坑点:迭代器是快照语义,读时看不到并发写入(符合其设计)。

三、内存成本的直觉刻度(64 位 + 压缩 OOP 场景的粗量级)

只给相对结论,避免平台细节差异误导:

  • ArrayList/ArrayDeque:一块数组 + 若干空槽引用;每元素仅 1 个引用位(外加数组对象头)。
  • LinkedList每元素额外 2 个引用 + 1 个节点对象头,远高于动态数组;局部性差,CPU 预取命中低。
  • CopyOnWriteArrayList:与 ArrayList 类似,但写入瞬间会出现“一旧一新两块数组”的临时双倍占用

四、扩容与“摊还 O(1)”的直观解释

  • 动态数组类(ArrayList/ArrayDeque)在空间满时做几何扩张(如 1.5× / 2×),虽然某次扩容是 O(n) 拷贝,但把成本到每次尾插上,平均下来仍是 O(1)
  • 若能提前知道规模,ensureCapacity(或预估初始容量)可以显著减少扩容次数与总拷贝量。
  • 为什么不自动收缩? 收缩同样要 O(n) 拷贝,还可能马上又扩回去,导致“抖动”。因此主流实现默认不收缩,交给业务在“规模长期下降”时手动 trim/rebuild。

五、选型清单(实践版)

  • 绝大多数列表:ArrayList / Kotlin MutableList(底层就是 ArrayList)。
  • 队列/栈/双端:ArrayDeque(比 LinkedList 更好)。
  • 读多写少、监听器:CopyOnWriteArrayList。
  • 只有在“已定位节点 + 高频插删” :LinkedList 才有优势。
  • Android 额外提示:不要用 LinkedList 做 RecyclerView 数据源;大数据量时 ArrayList 的局部性胜出明显。

六、两段常用代码模板

ArrayList 提前扩容

var list = new ArrayList<Foo>(/* expectedSize */ 10_000);
// 批量 add 期间避免多次扩容

ArrayDeque 作为队列/栈

Deque<Task> q = new ArrayDeque<>();
q.offer(task);      // 入队 O(1)
Task t = q.poll();  // 出队 O(1)

ArrayDeque<String> stack = new ArrayDeque<>();
stack.push("a");    // 压栈 O(1)
stack.pop();        // 弹栈 O(1)

一句话总结

默认 ArrayList,队列用 ArrayDeque;读多写少用 CopyOnWriteArrayList;只有“已定位节点 + 高频插删”再考虑 LinkedList。****

在需要高性能时,预估容量、避免中部操作、用连续内存争取缓存友好,是最划算的三件事。