可变长数组
Java: ArrayList
Python: List
如何实现一个变长数组?
- 支持索引与随机访问 -> set(i, val) get(i) 边界检查
- 分配多长的连续空间? -> capacity
- 空间不够用了怎么办?-> push back
- 空间剩余很多如何回收?-> pop back
一个简易的可变长数组实现
- 初始: 空数组,分配常数空间(也可以初始为null,在第一次push时进行分配常数空间),记录size和capacity。
- Push back:若空间不够,重新申请2倍大小的连续空间,拷贝到新空间,释放就空间。
- 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 类实现了三个构造函数:
- 创建 ArrayList 对象时,传入一个初始化值;
- 默认创建一个空数组对象;
- 传入一个集合类型进行初始化。
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的两种方式
- 直接将元素加到数组的末尾 O(1)
- 添加元素到任意位置。 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];
}