Skip to content

动态数组

引言

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

起源与发展

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

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

基本概念

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

核心操作

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

实现

java实现

  • 获取元素的数量
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;
}

具体的代码在 动态数组

php实现

  • 获取元素数量
php
public function size(): int
{
    return $this->size;
}
  • 判断是否为空
php
public function isEmpty(): bool
{
    return $this->size === 0;
}
  • 清空动态数组
php
public function clear(): void
{
    // 不使用unset()或array_fill()方法,专注于数据结构和算法的思路
    for ($i = 0; $i < count($this->elements); $i++) {
        $this->elements[$i] = null;
    }
    $this->size = 0;
}
  • 获取指定索引的元素,获取元素前先要验证索引的合法性
php
public function get(int $index): int
{
    $this->checkIndex($index);
    return $this->elements[$index];
}
  • 修改指定索引的元素,返回索引之前的元素
php
public function set(int $index, int $element): int
{
    $this->checkIndex($index);
    $oldElement = $this->elements[$index];
    $this->elements[$index] = $element;
    return $oldElement;
}
  • 添加元素到指定索引,返回索引之前的元素
php
public function addAt(int $index, int $element): void
{
    $this->checkIndexForAdd($index);
    // 扩容
    $this->expansion($index + 1);
    for ($i = $this->size; $i > $index; $i--) {
        $this->elements[$i] = $this->elements[$i - 1];
    }
    $this->elements[$index] = $element;
    $this->size++;
}
  • 删除指定索引的元素,返回索引之前的元素
php
public function remove(int $index): int
{
    $this->checkIndex($index);
    // 保存索引之前的元素,用于返回
    $oldElement = $this->elements[$index];
    for ($i = $index; $i < $this->size - 1; $i++) {
        $this->elements[$i] = $this->elements[$i + 1];
    }
    $this->size--;
    $this->shrinking();
    return $oldElement;
}
  • 获取指定元素的索引,如果是null,返回第一个是null的元素的索引
php
public function indexOf(int $element): int
{
    if ($element === null) {
        for ($i = 0; $i < $this->size; $i++) {
            if ($this->elements[$i] === null) {
                return $i;
            }
        }
    } else {
        for ($i = 0; $i < $this->size; $i++) {
            if ($this->elements[$i] === $element) {
                return $i;
            }
        }
    }
    return self::ELEMENT_NOT_FOUND;
}
  • 是否包含指定元素
php
public function contains(int $element): bool
{
    return $this->indexOf($element) !== self::ELEMENT_NOT_FOUND;
}

具体的代码在 动态数组

应用场景

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

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

优点

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

缺点

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

性能分析

时间复杂度

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

空间复杂度

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

未来展望

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

总结

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