数据结构2 线性表 - 数组 - 设计可变长数组

89 阅读2分钟
可变长数组

Java: ArrayList
Python: List

如何实现一个变长数组?

  1. 支持索引与随机访问 -> set(i, val) get(i) 边界检查
  2. 分配多长的连续空间? -> capacity
  3. 空间不够用了怎么办?-> push back
  4. 空间剩余很多如何回收?-> pop back

一个简易的可变长数组实现

  1. 初始: 空数组,分配常数空间(也可以初始为null,在第一次push时进行分配常数空间),记录size和capacity。
  2. Push back:若空间不够,重新申请2倍大小的连续空间,拷贝到新空间,释放就空间。
  3. Pop back:若空间利用率 size/capacity不到25%,释放一半的空间。

插入均摊的时间复杂度为2n/n -> O(1)
在空数组中连续插入n个元素,总插入/拷贝次数为n + n/2 + n/4 + ... < 2n
一次扩容到下一次释放,至少需要再删除 n - 2n * 0.25 0.5n次

Tips: 如果Pop back的阈值设置为50%,会导致频繁进行缩容和扩容,性能急剧抖动。

Java实现
ArrayList核心属性

// 默认初始化容量
 private static final int DEFAULT_CAPACITY = 10;
 // 对象数组
 transient Object[] elementData; 
 // 数组长度
 private int size;

构造函数
ArrayList 类实现了三个构造函数:

  1. 创建 ArrayList 对象时,传入一个初始化值;
  2. 默认创建一个空数组对象;
  3. 传入一个集合类型进行初始化。

Tips: 当 ArrayList 新增元素时,如果所存储的元素已经超过其已有大小,它会计算元素大小后再进行动态扩容,数组的扩容会导致整个数组进行一次内存复制。因此,我们在初始化ArrayList 时,可以通过第一个构造函数合理指定数组初始大小,这样有助于减少数组的扩容次数,从而提高系统性能。

public ArrayList(int initialCapacity) {
 // 初始化容量不为零时,将根据初始化值创建数组大小
 if (initialCapacity > 0) {
 this.elementData = new Object[initialCapacity];
 } else if (initialCapacity == 0) {// 初始化容量为零时,使用默认的空数组
 this.elementData = EMPTY_ELEMENTDATA;
 } else {
 throw new IllegalArgumentException("Illegal Capacity: "+
 initialCapacity);
 }
 }
 
 public ArrayList() {
 // 初始化默认为空数组
 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 }

Add的两种方式

  1. 直接将元素加到数组的末尾 O(1)
  2. 添加元素到任意位置。 O(n)
public boolean add(E e) {
 ensureCapacityInternal(size + 1); // Increments modCount!!
 elementData[size++] = e;
 return true;
 }
 
 public void add(int index, E element) {
 rangeCheckForAdd(index);
 
 ensureCapacityInternal(size + 1); // Increments modCount!!
 System.arraycopy(elementData, index, elementData, index + 1,
 size - index);
 elementData[index] = element;
 size++;
 }

扩容
ArrayList在添加元素之前,都会先确认容量大小,如果容量够大,就不用进行扩容;如果容量不够大,就会按照原来数组的 1.5 倍大小进行扩容,在扩容之后需要将数组复制到新分配的内存地址。

private void ensureExplicitCapacity(int minCapacity) {
 modCount++;
 
 // overflow-conscious code
 if (minCapacity - elementData.length > 0)
 grow(minCapacity);
 }
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
 
 private void grow(int minCapacity) {
 // overflow-conscious code
 int oldCapacity = elementData.length;
 int newCapacity = oldCapacity + (oldCapacity >> 1);
 if (newCapacity - minCapacity < 0)
 newCapacity = minCapacity;
 if (newCapacity - MAX_ARRAY_SIZE > 0)
 newCapacity = hugeCapacity(minCapacity);
 // minCapacity is usually close to size, so this is a win:
 elementData = Arrays.copyOf(elementData, newCapacity);
 }

Remove
ArrayList 在每一次有效的 删除元素操作之后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大。

public E remove(int index) {
 rangeCheck(index);
 
 modCount++;
 E oldValue = elementData(index);
 
 int numMoved = size - index - 1;
 if (numMoved > 0)
 System.arraycopy(elementData, index+1, elementData, index,
 numMoved);
 elementData[--size] = null; // clear to let GC do its work
 
 return oldValue;
 }

按索引读取
ArrayList 是基于数组实现的,所以在获取元素的时候是非常快捷的。

public E get(int index) {
 rangeCheck(index);
 
 return elementData(index);
 }
 
 E elementData(int index) {
 return (E) elementData[index];
 }