一、数组
关于数组,指的就是一组有限的相关类型的变量集合。在Java语言中,简单数组并没有像Collections集合的相关操作接口,本文将对简单数组封装相应的操作接口从而形成类ArrayList的集合类为目标,并进行代码时间复杂度分析以及代码优化
在开始之前,我们先对构造的Array类进行成员变量的说明:
- data:核心操作数组成员变量,为适应多种数据类型数据存储,将data设置为泛型数组。
- size:当前数组的元素个数控制指针(手动操作作用于数组上的元素个数,在Java语言中开辟数组会将数组元素进行初始化,这点我们忽略),也就是说size指针会永远指向当前已存在元素的下一个索引位置。例如:数组容量为10,当前已存在数组元素为a[0],a[1],a[2],则size指针便指向 a[3] 元素空间上,并表示当前数组存在元素为 3 (这里排除随机存储数据元素的情况,以上情况会很大程度上浪费存储空间,不建议使用)
相应图解如下:
Part 1 基于泛型的数组类的实现
/**
* @Author: Jiangyanfei
* @Date: 2019/4/26 11:35
* @Version 1.0
*/
public class Array<E> {
/**
* 泛型数组
*/
private E[] data;
/**
* 数组实际元素个数
*/
private int size;
public Array(int capacity) {
data = (E[]) new Object [capacity];
size = 0;
}
public Array() {
this(10);
}
/**
* 获取数组元素的个数
*/
public int getSize() {
return size;
}
/**
* 获取数组容量
*/
public int getCapacity() {
return data.length;
}
/**
* 判断数组是否为空
*/
public boolean isEmpty() {
return size == 0;
}
}
基础的属性方法之后,要开始对常规的操作接口 [CURD] 的设计。先对数组元素的查询和修改进行方法设计,对于存在数组容量变更的添加和删除操作在后面会说明。
- 查询
-
获取index索引处的元素
/** * 获取index索引处的元素 */ public E get(int index) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } return data[index]; }
-
查询数组中是否有元素 e
/** * 查询数组是否存在元素e */ public boolean contains(E e) { for (E element : data) { if (element.equals(e)) { return true; } } return false; }
-
查询数组中元素e是否存在,返回索引,不存在返回 -1
/** * 查询数组中元素e是否存在,返回索引,不存在返回 -1 */ public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; }
-
- 修改
-
改变index索引出的元素值
/** * 改变index索引处的元素值 */ public void set(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } data[index] = e; }
-
Part 2 动态数组实现
在开始正式内容之前,需要先提及一个问题,数组扩容问题?
为什么要数组扩容,在数组使用期间,尤其针对添加和删除操作,定长的数组长度会导致数组空间的浪费,这里提出一种数组扩容的方法:采用新开数组进行数组元素转移实现容量扩充或缩减。
采用数组转移的方式进行扩容方案需要考虑一个问题:扩容的幅度?
假设存在一个容量为10的数组空间(已满),这时候新添加数组元素,扩容的幅度为1?那么每次元素新添加都要进行扩容操作?反之,删除元素同理。频繁的调用扩容方法、大幅度扩大数组容量会造成剩余空间浪费,我们需要从这两种极端情况中寻找 折中策略
私有数组扩容方法:resize()
/**
* 数组扩容方法
*/
private void resize(int newCapacity) {
E[] newData = ((E[]) new Object[newCapacity]);
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
resize() 图示:
数组扩容或缩容的实质就是:数组元素移动到新数组中,并将数组地址指向新数组。
这里我们默认扩容幅度与缩容幅度倍数为:2 (仅举例,可自定义)
结合上述 resize()
方法,继续完成添加和删除操作方法的设计:
- 添加
-
指定索引添加元素
/** * 指定索引处添加元素 */ public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } // 数组扩容判断 if (size == data.length) { resize(data.length * 2); } // 插入元素核心代码,指定索引后数组元素整体后移 for (int i = size -1; i >= index; i--) { data[i+1] = data[i]; } data[index] = e; size ++; }
-
- 删除
-
指定索引删除元素
/** * 指定索引处删除元素 */ public E remove(int index) { if (index < 0 || index > size) { throw new IllegalArgumentException("Index is incorrect"); } // 保存待删除数组元素 E res = data[index]; // 删除元素核心代码,指定索引后数组整体前移 for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size --; // 数组缩容判断 if (size == data.length / 2 && data.length /2 != 0) { resize(data.length / 2); } return res; }
-
Part 3 均摊复杂度浅析以及防止复杂度震动
在提及均摊复杂度之前,首先来分析数组容量变动所引发的时间复杂度变化:
以添加操作为例:假设数组当前 size 为 n
- 数组首部插入元素,索引直接定位到 0,索引后数组整体移动范围为 n
- 数组尾部插入元素,索引直接定位到 size,元素插入,原数组元素移动范围 0
- 数组指定索引插入元素,结合上述两种临界情况,原数组元素移动范围 [0, n]
综合来讲,添加操作的时间复杂度为 O(n),只有在尾部插入元素时,时间复杂度为 O(1)
那么我们假设一种情景,数组容量为 N 的空数组状态下,依次插入 N+1 个数组元素,并且触发数组容量扩容操作的流程。
- 首先,在空数组状态下依次插入 N 个元素,此时操作数:N
- 当数组插入第 N 个元素完成后,准备插入第 N+1 个元素时,触发
resize()
进行数组容量扩容。完成数组扩容操作后,需要将原数组元素进行转移,此时操作数变为了:N + N - 当完成数组转移之后,插入第 N+1 个元素。此时操作数为:2N + 1
我们插入了 N+1 个数组元素,总计操作数为:2N + 1,粗略计算平均每插入一个元素需要耗费的操作数为:2
那么这个平均的操作数消耗量我们可以作为均摊复杂度分析的依据,那么添加操作的均摊复杂度为:O(1)
再考虑一种情景,数组容量为 N 的空数组依次插入 N 个元素,当插入第 N+1 个元素后,便进行删除第 N+1 个元素的操作,完成之后继续插入第 N+1 个元素,删除第 N+1 个元素…如此反复的进行该流程。
上面说的场景的本质是,扩容和缩容的高频操作,每次扩容/缩容都需要将数组元素向新数组进行转移,且转移的大小与数组长度有关,这一数组转移操作的时间复杂度稳定在 O(n),那么反复的进行该类时间复杂度稳定的操作,会导致复杂度的震动现象。
针对上述复杂度震荡情况,改进下remove()
方法,在数组长度低于 data.length / 2
时触发resize()
方法。 将触发的条件 data.length / 2
进行 Lazy
延迟处理 resize()
方法的执行。
if (size == data.length / 4 && data.length /2 != 0) {
resize(data.length / 2);
}
在上述场景的缩容部分中,数组容量为 2N,resize()
触发条件为当数组长度变为:N/2 时(容量四分点)触发。
注:data.length /2 != 0
是确保不构造容量为 0 的新数组