数组

0 阅读4分钟

数组是由一组元素(值或变量)组成的数据结构,每个元素至少一个索引或者键来标识

特点:连续存储,可通过索引快速访问(O(1))

📊 ArrayList核心操作时间复杂度概览

首先,通过下表你可以快速了解各操作的时间复杂度概况。

操作方法示例平均/最差时间复杂度算法关键点
查 (Read)​​get(int index)​O(1)​基于数组的索引直接访问,一次计算直达目标。
改 (Update)​​set(int index, E element)​O(1)​类似查询,通过索引定位后直接替换引用。
增 (Insert)​​add(E e)​(尾部添加)O(1)​ (分摊)尾部追加。若容量不足,触发​扩容​(时间复杂度O(n))。
​add(int index, E element)​(指定位置)O(n)​需要将目标位置后的所有元素向后移动一位。
删 (Delete)​​remove(int index)​O(n)​删除后,需要将后续所有元素向前移动一位以填补空隙。
​remove(Object o)​O(n)​需先线性遍历找到目标元素,再执行删除操作。

🔍 深入算法原理

1. 查询 (get​) 与修改 (set​)

这两个操作是ArrayList​的优势所在,其高效性源于底层数据结构是​数组​。

  • ​算法描述​:对于任意给定的索引index​,访问其元素只需一步计算:内存起始地址 + index * 元素类型大小​。这是一个常数时间的操作,与数组长度无关。

  • ​代码示例​:

    // 以下操作都非常快速
    String element = list.get(5);   // 直接访问第6个元素
    list.set(0, "newValue");         // 直接修改第1个元素的引用
    

2. 添加 (add​)

添加操作的性能取决于插入点的位置。

  • 尾部添加 (add(E e)​)

    • ​算法描述​:这是最高效的添加方式。通常只需将新元素放入数组下一个空闲位置(elementData[size++] = e​)。唯一的开销是​扩容​:当数组已满时,需要创建一个新的、更大的数组(通常是原容量的1.5倍),并将所有现有元素从旧数组复制到新数组。这个复制过程是O(n)​ 的。
    • ​分摊时间复杂度​:虽然单次扩容成本高,但扩容操作发生的频率较低。因此,将多次添加操作的总成本平均下来,每个元素的添加成本仍是​O(1)​,这被称为​分摊常数时间​。
  • 指定位置插入 (add(int index, E element)​)

    • ​算法描述​:此操作需要“腾出空间”。它使用System.arraycopy()​将index​及其之后的所有元素向后移动一位,然后将新元素放入空出的index​位置。
    // 假设在索引2处插入元素
    // 原数组: [A, B, C, D, E]
    System.arraycopy(elementData, 2, elementData, 3, 3); // 将C, D, E向后移动
    // 移动后: [A, B, null, C, D, E]
    elementData[2] = newElement; // 插入新元素
    // 结果: [A, B, newElement, C, D, E]
    
    • ​时间复杂度​:移动的元素数量是 n - index​。在最坏情况(插入列表开头,index=0​)下,需要移动所有n​个元素,因此时间复杂度为​O(n)​。

3. 删除 (remove​)

删除操作与指定位置插入类似,都需要移动元素。

  • ​算法描述​:无论是按索引还是按值删除,核心步骤都是​填补空隙​。

    1. ​按索引删除​:直接移除该位置元素,然后将其后续元素向前移动一位。
    2. ​按值删除​:首先需要遍历数组以找到第一个与目标值匹配的元素的索引,这个过程本身在最坏情况下就是O(n),找到后再执行与按索引删除相同的移动操作。
    // 删除索引2的元素
    // 原数组: [A, B, C, D, E]
    fastRemove(2); // 内部调用System.arraycopy
    // 将D, E向前移动,覆盖C: [A, B, D, E, null]
    
  • ​时间复杂度​:由于最坏情况下需要遍历和移动n​个元素,两种删除方法的时间复杂度均为​O(n)​。


💡 核心实践建议

根据上述分析,在实际开发中运用ArrayList​时,请牢记以下几点:

  1. ​优先用于“查多改少”的场景​:ArrayList​的随机访问性能(O(1))极其出色,非常适合需要频繁按索引读取数据的场景。
  2. ​警惕中间位置的增删​:在列表开头或中间进行频繁的插入和删除操作,会导致大量的元素移动,性能开销很大(O(n))。如果这是你的主要操作模式,应考虑使用LinkedList​,其增删操作的时间复杂度为O(1)。
  3. 优化添加操作:预分配容量​ 如果你能预估数据量,在创建ArrayList​时指定一个合适的初始容量(new ArrayList<>(initialCapacity)​),可以避免或减少扩容带来的数组复制开销,显著提升批量添加元素的性能。

💎 总结

​ArrayList​是一个基于动态数组实现的强大集合。其查询和修改操作得益于数组的连续内存结构,具有O(1) 的极致性能。而增删操作的性能则取决于位置:在尾部添加效率很高(分摊O(1)),但在其他位置进行增删,由于需要移动元素,性能为O(n)。