动态数组
1. 引言
动态数组(Dynamic Array)是一种数据结构,它的大小可以根据需要动态变化,在计算机科学中被广泛应用于各种场景。与固定大小的静态数组不同,动态数组能够根据元素的增删自动调整其容量,具有更高的灵活性。
2. 起源与发展
动态数组的概念源于对传统静态数组的扩展。静态数组的大小在编译时就已确定,且无法改变,这限制了其在某些场景中的应用。而动态数组通过内存的动态分配和管理,允许在运行时改变数组的大小。
动态数组的实现最早出现在C语言的标准库中,通过使用malloc
和realloc
来实现数组大小的动态调整。在现代编程语言中,动态数组被广泛采用,如C++中的std::vector
,Java中的ArrayList
,以及Python中的list
和PHP中的array。这些容器提供了高度的灵活性,使得程序员不必担心数组大小的管理,从而大大提高了编程效率。
3. 基本概念与定式
1. 定义
动态数组是一个可以根据需求动态调整大小的数组。它的基本特性是能够在运行时扩展容量,以容纳更多元素,而不像静态数组那样预设一个固定大小。
2. 核心操作
- 插入元素:动态数组允许在数组的任意位置插入新元素。当数组已满时,会自动分配一个更大的内存空间,将原来的元素复制到新空间中。
- 删除元素:删除元素时,动态数组会调整数组的大小,以保持最高的空间利用率。
- 查找元素:与静态数组类似,动态数组可以通过索引进行直接访问。
- 扩容与缩容:当数组满时,动态数组通常会按一定的增长倍数扩容,如2倍,而当元素数量减少到一定的数量时,会自动缩容以释放不必要的空间。注意:扩容的倍数与缩容的倍数相乘等于1时,会触发复杂度震荡,需要避免这种情况。
3. 实现
java实现
- 首先,动态数组要实现List接口
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);
}
- 基本代码框架:
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);
}
}
- 辅助方法
// 索引越界异常
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);
- 获取元素的数量
public int size() {
return size;
}
- 判断是否为空
public boolean isEmpty() {
return size == 0;
}
- 清空动态数组,不使用Arrays.fill()方法,专注于数据结构和算法的思路
@Override
public void clear() {
for (int i = 0; i < elements.length; i++) {
elements[i] = null;
}
size = 0;
}
- 获取指定索引的元素,获取元素前先要验证索引的合法性
@Override
public E get(int index) {
checkIndex(index);
return elements[index];
}
- 修改指定索引的元素,返回索引之前的元素
@Override
public E set(int index, E element) {
checkIndex(index);
E oldElement = elements[index];
elements[index] = element;
return oldElement;
}
- 添加元素到指定索引,返回索引之前的元素, 先验证索引的合法性,添加操作可能会触发扩容操作, 添加的时候,是从数组的最后一个元素的位置向前,直到待插入位置,向后移动一位, 然后把元素插入到待插入位置
@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++;
}
- 添加元素到尾部
public void add(E element) {
add(size, element);
}
- 删除指定索引的元素,返回索引之前的元素, 先验证索引的合法性,然后从删除位置开始,向后直到最后一个元素的位置, 将元素向前移动一位,元素数量减一,可能会触发缩容操作
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的元素的索引
@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;
}
- 是否包含指定元素
public boolean contains(E element) {
return indexOf(element) != ELEMENT_NOT_FOUND;
}
4. 应用场景
动态数组广泛应用于需要频繁增加或删除元素的场景,尤其是在不确定最终数据规模时。以下是一些典型应用:
- 数据库管理系统:动态数组用于管理数据表中的记录。当记录数量不确定时,动态数组提供了灵活的内存管理,能够应对不断变化的需求。
- 购物车的商品:动态数组用于存储用户的购物车信息。能够应对不断增减商品的需求。
5. 优缺点分析
优点
- 灵活性高:动态数组的最大优点是它的大小可以在运行时动态调整,避免了预先设定固定大小的问题。
- 访问速度快:动态数组的访问时间复杂度为O(1),与静态数组相同,能够提供快速的随机访问。
- 空间利用率较高:当数组的大小适应当前元素数量时,动态数组的空间利用率较高。
缺点
- 扩容开销大:每当动态数组扩容时,系统需要为新数组分配内存,并将旧数组中的元素复制到新数组中,这个操作的时间复杂度为O(n),可能导致性能问题,尤其是在大规模数据时。
- 内存碎片:虽然动态数组在扩容时会释放旧数组的内存,但频繁的扩容和缩容操作可能会导致内存碎片化。
- 预估不准确时的效率问题:如果初始容量预估过小,扩容次数过多会影响性能;如果预估过大,则会浪费内存。
6. 性能分析
时间复杂度
- 插入元素:通常情况下,插入操作的时间复杂度为O(1),但在需要扩容时,扩容操作的时间复杂度为O(n),因为所有元素都需要被复制到新数组中。
- 查找元素:访问某个元素的时间复杂度为O(1),与静态数组相同。
- 删除元素:删除操作的时间复杂度为O(n),因为可能需要移动数组中的元素以填补空位。
空间复杂度
动态数组的空间复杂度为O(n),其中n是当前数组中元素的数量。由于动态数组会预留多余的空间以减少扩容的次数,其实际占用的内存通常大于元素实际所需的内存。
7. 未来展望
动态数组在未来可能会与其他数据结构结合,以适应更复杂的应用需求。例如,结合哈希表、链表或树结构,形成混合型的动态数组,以支持更高效的查找和修改操作。此外,随着硬件技术的发展,动态数组的扩容和内存管理技术也可能得到优化,降低扩容时的开销。
8. 总结
动态数组是一种非常灵活且高效的数据结构,适用于多种场景,尤其是在元素数量动态变化的情况下。它的优点包括灵活性、快速访问和较高的空间利用率,但也面临扩容开销和内存碎片等问题。在性能分析中,动态数组的时间复杂度和空间复杂度均较为理想,但扩容时的代价值得注意。随着技术的进步,动态数组有望在更多领域得到优化和应用。