动态数组时间复杂度分析
动态数组解决了数组大小固定不变的问题,但同时复杂化了增添和删除元素操作的时间复杂度分析,现在主要分析addLast和removeLast两个方法的时间复杂度。
一、addLast 方法的时间复杂度分析
addLast方法在数组末尾增添一个元素,通常情况下时间复杂度为O(1),代码如下:
public void addLast(E e) {
add(size, e);
}
调用的add()方法代码如下:
public void add(int index, E e) {
if (size == data.length) {
resize(data.length * 2);
}
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
for (int i = size - 1; i >= index; i--)
data[i + 1] = data[i];
data[index] = e;
size++;
}
注意,当size == data.length条件满足时,会执行resize()方法:
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity];
if (size >= 0) System.arraycopy(data, 0, newData, 0, size);
data = newData;
}
resize()将原数组中的所有元素复制到一个新数组中,这个过程的时间复杂度为O(n)
因此,这就会产生一个新问题,addLast方法通常情况下的时间复杂度为O(1),但是在扩展数组的这个操作时,时间复杂度会变为O(n),那么addLast方法的复杂度应该怎么算?
分析:
现在假设初始数组的大小为8,在执行 8+1 次addLast方法之后,会调用一次resize方法,也就是说,resize方法并不会每次都被调用,它的平均调用次数为(8+1+8)/9次。(分子的算法为执行的(8+1)次操作和复制会执行的8次操作;分母为8次复杂度为O(1)的常规addLast方法和最后一次会调用resize方法的addLast,共计9次),而平均操作次数仅仅约等于2次
将这个算法代数表达,即(2n+1)/(n+1) 2
因此插入的操作的均摊时间复杂度仅为O(1)
二、removeLast 方法的时间复杂度分析
该方法值得讨论的地方在于到底应该在什么时候缩减数组的大小
现假设每当数组的大小为原始大小的1/2时,就立即缩减数组大小为原来的1/2,但是考虑一下某种情况——如果反复调用removeLast()和addLast()方法,也就会反复执行resize()方法,此时的时间复杂度会变为指数级,为了处理这个问题,可以延迟缩减数组容量的时间,代码如下:
public E remove(int index) {
E result;
if (index < 0 || index >= size) {
throw new IllegalArgumentException("参数错误");
}
result = data[index];
for (int i = index + 1; i < size; i++) { // 6
data[i - 1] = data[i];
}
size--;
// if 中需要满足两个条件,
// 前者是为了解决 addLast 和 removeLast 带来的 复杂度震荡 问题,
// 后者是为了解决当数组 size 为 0 时,依旧会满足判断而带来的 bug
if (size == data.length / 4 && (data.length / 2 != 0)) {
resize(getCapacity() / 2);
}
return result;
}
注释部分解答判断条件的由来。