动态数组(Dynamic Array)

811 阅读3分钟

动态数组时间复杂度分析

动态数组解决了数组大小固定不变的问题,但同时复杂化了增添和删除元素操作的时间复杂度分析,现在主要分析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) \approx 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;
}

注释部分解答判断条件的由来。