动态数组接口设计
定义
Java:在堆中申请的连续内存空间
int size()
元素的数目boolean isEmpty()
是否为空boolean contains(E element)
是否包含某个元素void add(E element)
添加一个元素到最后面E get(int index)
返回index位置对应的元素E set(int index, E element)
设置Index位置的元素void add(int index, E element)
向index位置插入一个元素E remove(int index)
移除index位置的元素并返回int indexOf(E element)
查看元素的位置void clear()
清除所有元素
动态数组的成员变量
int size
元素个数int[] elements
元素数组
构造方法
可以指定初始容量,也可以不指定,采用默认初始容量
如果指定容量小于默认容量,默认按照默认大小初始化数组
toString方法
isEmpty方法
get方法
时间复杂度分析:数组连续内存地址寻址,复杂度为O(1)
set方法
indexOf方法
复杂度O(1)
contains方法
clear方法
- 考虑缩容
add方法
在数组尾部添加一个元素
- 尾部的
下标
正好等于size
的值
info
可以根据add(int index, E element)
方法进行重构
时间复杂度:永远在数组的最后面添加
最好情况,直接在末尾添加,
O(1)
最差情况,在末尾添加时出发了数组扩容:创建新数组,旧数组遍历复制到新数组
O(n)
平均复杂度:n-1个O(1),和1个O(n) -> [(n-1) + n] / n = (2n-1) / n -> 极限 -> 2n /n = 2 ->
O(1)
均摊复杂度:
O(1)
将最后扩容那一次的时间开销分摊到每一次操作中去- 未触发扩容时每次执行复杂度为
O(1)
,共n-1
次 - 触发扩容时复杂度为
O(n)
- 将
O(n)
分摊到n-1
次O(1)
上去 - 大致为每次操作
O(2)
即复杂度为O(1)
- 未触发扩容时每次执行复杂度为
使用均摊复杂度的场景:连读大量复杂度很低的操作后,突然出现少量复杂度较高的操作
remove方法
- 记录被删除的元素的值
- 所有后面的元素向前移动
- size--
- 时间复杂度——与添加方法同理,时间复杂度为
O(n)
add(int index, E element)方法
在指定索引位置添加元素
- 倒着挪动,从size-1挪动到size
- size-2挪动到size-1
- 一共挪动size - index 次
- 挪动范围 [index, size-1]
时间复杂度:数据规模为n,此处数据规模取决于size
最好情况复杂度 index = size
在最后添加一个元素,直接寻址到追后面,不需要元素的移动,
O(1)
最差情况复杂度 移动size次
插在最前面,后面的所有元素都需要移动,
O(n)
平均复杂度
[O(1) + O(2) + ... + O(n)] / n = (n + 1)n / n , 当 n->无穷 ,等于 n^2 / n = n
O(n)
下标越界检查函数
扩容
- 申请一段新的内存空间
- 将原数组的内容复制过来
- 将引用指向新数组
- 垃圾回收机制自动回收旧数组
info
在添加时判断是否扩容
- 根据elements数组的长度得到现在的容量
- 如果现在的容量大于指定的容量,则扩容函数直接返回
- 否则扩容,申请新数组,大小为旧数组的1.5倍
- 将旧数组的值复制到新数组中(不考虑线程安全)
- 将引用指向新数组,旧数组被垃圾回收器回收
在add方法前,添加扩容方法,指定的确保容量为size + 1
执行测试
缩容
和扩容类似,还是开辟新的数组空间
- 在
remove
方法执行期间,考虑对数组进行缩容- 在
clear
方法执行期间,考虑对数组进行缩容- 缩容触发条件(自己定义):SIZE为总容量(数组的length)的一半时,就进行缩容
- 要求当前容量不能比默认容量小,否则缩到很小后,频繁触发扩容缩容影响效率
- 确定新数组的容量
- 测试
泛型
定义并使用泛型的步骤
- 在类后面指定泛型
class MyArrayList<E>
- 类中方法的返回类型都可以写成
E
- 泛型类型的数组通过Object类型在堆内存中申请,再强转成需要的类型
对象数组的存储方式
对象数组中存储的不是对象本身,而是对象在堆中的地址值
修改clear方法适应泛型
为什么需要修改
如果不做处理,那么对象的地址存在对象数组中,却没有人用,但引用是实实在在存在的,那么就造成了内存泄漏
- 应当将对象数组的全部位置设置为
null
查看对象被垃圾回收器回收
可以重写类的finalize()
方法,该方法在对象即将被销毁时被调用
- 通过
System.gc()
,主动触发垃圾回收
修改remove方法适应泛型
为什么要修改
执行remove
方法时,进行了元素的移动,在数组的尾部空出了一个空间,需要对其进行处理,否则会产生内存泄漏
修改indexOf方法适应泛型
为什么修改
在原先的indexOf方法中,通过==
判断传入的值与数组中的值是否相等,在泛型下,传入的参数可能为引用类型,也就是可能是一个对象,对象间的判等不能使用==
来进行,因为==
判断的是对象的地址,两个各种属性一致的对象可能具有不同的地址从而被==
判断为不相等
- 应当使用
equls()
方法来进行判等,可以通过重写来自定义判断的方式
修改indexOf方法适应null值的搜索
为什么修改
因为如果数组中存储了null,在调用equals方法是会引发空指针异常