Skip to content

动态数组

1. 引言

动态数组(Dynamic Array)是一种数据结构,它的大小可以根据需要动态变化,在计算机科学中被广泛应用于各种场景。与固定大小的静态数组不同,动态数组能够根据元素的增删自动调整其容量,具有更高的灵活性。

2. 起源与发展

动态数组的概念源于对传统静态数组的扩展。静态数组的大小在编译时就已确定,且无法改变,这限制了其在某些场景中的应用。而动态数组通过内存的动态分配和管理,允许在运行时改变数组的大小。

动态数组的实现最早出现在C语言的标准库中,通过使用mallocrealloc来实现数组大小的动态调整。在现代编程语言中,动态数组被广泛采用,如C++中的std::vector,Java中的ArrayList,以及Python中的list和PHP中的array。这些容器提供了高度的灵活性,使得程序员不必担心数组大小的管理,从而大大提高了编程效率。

3. 基本概念与定式

1. 定义

动态数组是一个可以根据需求动态调整大小的数组。它的基本特性是能够在运行时扩展容量,以容纳更多元素,而不像静态数组那样预设一个固定大小。

2. 核心操作

  1. 插入元素:动态数组允许在数组的任意位置插入新元素。当数组已满时,会自动分配一个更大的内存空间,将原来的元素复制到新空间中。
  2. 删除元素:删除元素时,动态数组会调整数组的大小,以保持最高的空间利用率。
  3. 查找元素:与静态数组类似,动态数组可以通过索引进行直接访问。
  4. 扩容与缩容:当数组满时,动态数组通常会按一定的增长倍数扩容,如2倍,而当元素数量减少到一定的数量时,会自动缩容以释放不必要的空间。注意:扩容的倍数与缩容的倍数相乘等于1时,会触发复杂度震荡,需要避免这种情况。

3. 实现

java实现

  • 首先,动态数组要实现List接口
java
package com.hxqzzxk.list;

// 定义线性表相关的接口
public interface List<E> {
    // 清空元素
    void clear();
    // 返回元素的数量
    int size();
    // 是否为空
    boolean isEmpty();
    // 是否包含指定元素
    boolean contains(E element);
    // 添加元素到尾部
    void add(E element);
    // 获取指定位置的元素
    E get(int index);
    // 设置指定位置的元素,返回该位置之前的元素
    E set(int index, E element);
    // 在指定位置插入一个元素
    void add(int index, E element);
    // 删除指定位置的元素,返回该位置之前的元素
    E remove(int index);
    // 查看指定元素的位置
    int indexOf(E element);
}
  • 基本代码框架:
java
package com.hxqzzxk.list;

// 动态数组
@SuppressWarnings("unchecked")
public class ArrayList<E> extends AbstractList<E> {
    // 找不到元素返回的索引值
    protected static final int ELEMENT_NOT_FOUND = -1;
    // 元素数量
    protected int size;
    // 实际存储元素的数组
    private E[] elements;

    // 数组的默认容量
    private static final int DEFAULT_CAPACITY = 10;

    // 构造函数
    public ArrayList(int capacity) {
        // 当容量小于等于默认容量,使用默认容量
        capacity = Math.max(capacity, DEFAULT_CAPACITY);
        elements = (E[]) new Object[capacity];
    }

    // 默认构造函数,使用默认的容量
    public ArrayList() {
        this(DEFAULT_CAPACITY);
    }
}
  • 辅助方法
java
// 索引越界异常
private void outOfBounds(int index) {
    throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
}

// 确认索引没有越界
private void checkIndex(int index) {
    if (index < 0 || index >= size) {
        outOfBounds(index);
    }
}

// 针对添加操作,确认索引没有越界,添加操作可以在线性表的最后位置添加元素
private void checkIndexForAdd(int index) {
    if (index < 0 || index > size) {
        outOfBounds(index);
    }
}

// 确认是数组容量是否满足
private boolean checkCapacity(int capacity) {
    return elements.length >= capacity;
}

// 扩容,数组容量不够,扩容加大容量到原始容量的1.5倍
private void expansion(int capacity) {
    if (checkCapacity(capacity)) {
        return;
    }
    int oldCapacity = elements.length;
    // 扩容后的容量是旧容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    @SuppressWarnings("unchecked")
    E[] newElements = (E[]) new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        newElements[i] = elements[i];
    }
    elements = newElements;
}

// 缩容,元素数量小于等于数组容量的一半时,缩容到一半
private void shrinking() {
    int oldCapacity = elements.length;
    // 缩容后的容量是旧容量的一半
    int newCapacity = oldCapacity >> 1;
    // 如果当前的元素数量大于新容量或者新容量小于默认容量,不需要缩容
    if (size > newCapacity || newCapacity < DEFAULT_CAPACITY) {
        return;
    }

    E[] newElements = (E[]) new Object[newCapacity];
    // 注意:不使用Arrays.fill()方法,专注于数据结构和算法的思路
    for (int i = 0; i < size; i++) {
        newElements[i] = elements[i];
    }
    elements = newElements;
}

// 打印动态数组
@Override
public String toString() {
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("size: ").append(size).append(", elements: [");
    for (int i = 0; i < size; i++) {
        if (i != 0) {
            stringBuilder.append(", ");
        }
        stringBuilder.append(elements[i].toString());
    }
    return stringBuilder.append("]").toString();
}

注意:扩容的倍数 x 和缩容的倍数 y ,x * y 不能等于1,否则会触发复杂度震荡 比如:capacity = 20,size = 10,那就会触发缩容,capacity = 20 / 2 = 10,此时再次add,size = 11,触发扩容,capacity = 10 * 2 = 20,然后再次remove,重复以上操作,就会一直在缩容和扩容之间进行切换,从而触发复杂度震荡,导致add和remove操作的时间复杂度由O(1)变为O(n);

  • 获取元素的数量
java
public int size() {
    return size;
}
  • 判断是否为空
java
public boolean isEmpty() {
    return size == 0;
}
  • 清空动态数组,不使用Arrays.fill()方法,专注于数据结构和算法的思路
java
@Override
public void clear() {
    for (int i = 0; i < elements.length; i++) {
        elements[i] = null;
    }
    size = 0;
}
  • 获取指定索引的元素,获取元素前先要验证索引的合法性
java
@Override
public E get(int index) {
    checkIndex(index);
    return elements[index];
}
  • 修改指定索引的元素,返回索引之前的元素
java
@Override
public E set(int index, E element) {
    checkIndex(index);
    E oldElement = elements[index];
    elements[index] = element;
    return oldElement;
}
  • 添加元素到指定索引,返回索引之前的元素, 先验证索引的合法性,添加操作可能会触发扩容操作, 添加的时候,是从数组的最后一个元素的位置向前,直到待插入位置,向后移动一位, 然后把元素插入到待插入位置
java
@Override
public void add(int index, E element) {
    checkIndexForAdd(index);
    // 扩容
    expansion(index + 1);
    for (int i = size; i > index; i--) {
        elements[i] = elements[i - 1];
    }
    elements[index] = element;
    size++;
}
  • 添加元素到尾部
java
public void add(E element) {
    add(size, element);
}
  • 删除指定索引的元素,返回索引之前的元素, 先验证索引的合法性,然后从删除位置开始,向后直到最后一个元素的位置, 将元素向前移动一位,元素数量减一,可能会触发缩容操作
java
public E remove(int index) {
    checkIndex(index);
    // 保存索引之前的元素,用于返回
    E oldElement = elements[index];
    for (int i = index; i < size - 1; i++) {
        elements[i] = elements[i + 1];
    }
    size--;
    shrinking();
    return oldElement;
}
  • 获取指定元素的索引,如果是null,返回第一个是null的元素的索引
java
@Override
public int indexOf(E element) {
    if (element == null) {
        for (int i = 0; i < size; i++) {
            if (elements[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = 0; i < size; i++) {
            if (elements[i].equals(element)) {
                return i;
            }
        }
    }
    return ELEMENT_NOT_FOUND;
}
  • 是否包含指定元素
java
public boolean contains(E element) {
    return indexOf(element) != ELEMENT_NOT_FOUND;
}

4. 应用场景

动态数组广泛应用于需要频繁增加或删除元素的场景,尤其是在不确定最终数据规模时。以下是一些典型应用:

  1. 数据库管理系统:动态数组用于管理数据表中的记录。当记录数量不确定时,动态数组提供了灵活的内存管理,能够应对不断变化的需求。
  2. 购物车的商品:动态数组用于存储用户的购物车信息。能够应对不断增减商品的需求。

5. 优缺点分析

优点

  1. 灵活性高:动态数组的最大优点是它的大小可以在运行时动态调整,避免了预先设定固定大小的问题。
  2. 访问速度快:动态数组的访问时间复杂度为O(1),与静态数组相同,能够提供快速的随机访问。
  3. 空间利用率较高:当数组的大小适应当前元素数量时,动态数组的空间利用率较高。

缺点

  1. 扩容开销大:每当动态数组扩容时,系统需要为新数组分配内存,并将旧数组中的元素复制到新数组中,这个操作的时间复杂度为O(n),可能导致性能问题,尤其是在大规模数据时。
  2. 内存碎片:虽然动态数组在扩容时会释放旧数组的内存,但频繁的扩容和缩容操作可能会导致内存碎片化。
  3. 预估不准确时的效率问题:如果初始容量预估过小,扩容次数过多会影响性能;如果预估过大,则会浪费内存。

6. 性能分析

时间复杂度

  1. 插入元素:通常情况下,插入操作的时间复杂度为O(1),但在需要扩容时,扩容操作的时间复杂度为O(n),因为所有元素都需要被复制到新数组中。
  2. 查找元素:访问某个元素的时间复杂度为O(1),与静态数组相同。
  3. 删除元素:删除操作的时间复杂度为O(n),因为可能需要移动数组中的元素以填补空位。

空间复杂度

动态数组的空间复杂度为O(n),其中n是当前数组中元素的数量。由于动态数组会预留多余的空间以减少扩容的次数,其实际占用的内存通常大于元素实际所需的内存。

7. 未来展望

动态数组在未来可能会与其他数据结构结合,以适应更复杂的应用需求。例如,结合哈希表、链表或树结构,形成混合型的动态数组,以支持更高效的查找和修改操作。此外,随着硬件技术的发展,动态数组的扩容和内存管理技术也可能得到优化,降低扩容时的开销。

8. 总结

动态数组是一种非常灵活且高效的数据结构,适用于多种场景,尤其是在元素数量动态变化的情况下。它的优点包括灵活性、快速访问和较高的空间利用率,但也面临扩容开销和内存碎片等问题。在性能分析中,动态数组的时间复杂度和空间复杂度均较为理想,但扩容时的代价值得注意。随着技术的进步,动态数组有望在更多领域得到优化和应用。