算法学习笔记01 动态数组的扩容机制

153 阅读3分钟

数组

数组是内存中的一块连续的空间,里面存储的元素都是相同的,并且按顺序存储,所以支持按索引直接读取数据。根据上面我们可以得出数组的两个重要属性,“存储类型 和 大小”

数组使用时需要提前向内存申请空间大小,这在实际应用中不是很方便。

比如我们使用数组来存储学校的学生信息,因为学生的信息总是在增加,而我们申请的大小的固定的,就存在数组溢出的情况。

动态数组

为了解决这个问题,出现了动态数组,及数组的大小不是固定的。那么大小不固定,我们就需要知道开始位置、结束位置、使用的最后一个位置和存储类型

在Java中,动态数组并没有提供查看数组容量的参数,但是在C++中提供了,我们可以作为参考

template<class_Tp,class_Alloc=__STL_DEFAULT_ALLOCATOR(_Tp)>
classvector:protected_Vector_base<_Tp,_Alloc>
    {
        ...protected:_Tp*_M_start;//表示目前使用空间的头
        _Tp*_M_finish;//表示目前使用空间的尾
        _Tp*_M_end_of_storage;//表示目前可用空间的尾
        ...
    };

使用这几个参数我们就可以知道数据的使用情况。

当前元素个数 = _M_finish - _M_start

数组容量 = _M_end_of_storage - _M_start

我们编写了这样一个代码,创建了一个存储int类型的动态数组

public static void main(String[] args) {
    ArrayList list =  new ArrayList<Integer>();
    list.add(123);
}

默认是向数组的最后一位添加数据

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

再进入add我们发现,开始逻辑判定,先判断当前数组是否已经走到末尾,如果走到了,需要进行扩容。如果没有走到,那么直接向数组的后面添加数据。

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

再进入我们来到这里,这里的minCapacity是当前数组的容量+1。

graph TD
如果数组不是空 --> 数组扩容扩大0.5倍 --> 拷贝原来的数据到新的数组
如果数组不为空 --> 将现有申请的长度与DEFAULT_CAPACITY进行比较申请空间 -->返回新数组

DEFAULT_CAPACITY = 10

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

那么为什么要这么扩容?

在C++ 中为两倍扩容大小,这就存在一个问题无法利用已经释放的内存空间。

我们假设已经进行了n次扩容,那么当前的空间就为2n+C2^n+C我们忽略这个常数C那么扩容的空间就为2n2^n

之前释放掉的大小就为i=0n12xi=2n1\sum_{i=0}^{n-1} 2^{x_i} = 2^n - 1

假设扩容的倍率为XX,首次分配的空间为1,则第一次扩容分配的空间为XX,第二次扩容分配的空间就X2X^2,如果希望第二次扩容能用上之前申请过的空间,那么

1+X=X21 + X = X ^ 2

解方程得 X=1.618X = 1.618

如果希望第二次扩容能用上第一次的空间,那么需要扩容倍率小于1.618。

如果我们需要缩容,那么该怎么做

在Java的ArrayList中,没有自动缩容,但是提供了一个trimToSize()的方法可以让使用者手动缩容;如果要设计自动缩容的话,可以简单设计成size减少到到capacity/4的时候,将容量缩减成原来的一半。

复杂度震荡:
如果我们在扩容时设计的是容量是2倍,缩容设计的是旧容量的一半,此时如果我们在容量满的时候插入一个数据,会导致扩容,然后在删除一个元素,又会发生缩容,这样的复杂度就是O(n), 就会出现复杂度震荡