Array学习笔记
- Array是什么?
答:简单来说,就是数据的一种有序排列。我们把数据码排成一排进行存放,并且通过索引可以方便地获得对应位置的数据信息。索引可以分为有语意和无语意两种情况,一般来说,数组更适合有语意的应用场景。
- Array在内存是怎么样的?
答:一块数组容量大小的内存区域在jvm上分配,索引的物理地址应该是连续分布的。
自制数组
package Array;
public class Array<E> {
private E[] data;
private int size;
//传入容量capacity的构造方法
public Array(int capacity) {
//泛型数组的初始化
data = (E[]) new Object[capacity];
size = 0;
}
//无参构造方法
public Array() {
this(10);
}
//获取数组的容量
public int getCapacity() {
return data.length;
}
//获取数组中元素的个数
public int getSize() {
return size;
}
//判断数组是否为空
public boolean isEmpty() {
return size == 0;
}
//在索引index处添加元素
public void add(int index, E e) {
//边界判断
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Required index >= 0 and index <= size");
}
//数组容量不足,重设容量
if (size == data.length) {
resize(2 * data.length);
}
//将index后的元素向后移移一位
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
//维护size
size++;
}
//在第一个位置添加一个元素
public void addFirst(E e) {
add(0, e);
}
//在所有元素后添加一个元素
public void addLast(E e) {
add(size, e);
}
//获取索引为index的元素
public E get(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Index is illegal.");
}
return data[index];
}
//修改index索引位置的元素为e
public void set(int index, E e) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set Failed. Index is illegal.");
}
data[index] = e;
}
//查找数组中是否有元素e
public boolean contains(E e) {
for (int i = 0; i < size; i++) {
//注意用equals别用 ==
if (data[i].equals(e)) {
return true;
}
}
return false;
}
//查找元素e所在的索引,如果存在返回第一个索引,如果不存在,返回-1
public int find(E e) {
for (int i = 0; i < size; i++) {
if (data[i].equals(e)) {
return i;
}
}
return -1;
}
//从数组中删除index位置上的元素,并返回其值
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Removed failed. Index is illegal.");
}
E ret = data[index];
//将index后的所有元素全部向前挪一个位置
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
//维护size
size --;
//当删除后元素数量小到一定程度时,进行降容处理
if (size == data.length / 2) {
resize(data.length / 2);
}
return ret;
}
//从数组中删除第一个元素
public E removeFirst() {
return remove(0);
}
//从数组中删除最后一个元素
public E removeLast() {
return remove(size - 1);
}
//从数组中删除元素为e的元素(第一个e元素)
public void removeElement(E e) {
int index = find(e);
if (index != -1) {
remove(index);
}
}
//重新给数组设定容量
private void resize(int newCapacity) {
//申请新容量的数组
E[] newData = (E[]) new Object[newCapacity];
//将旧数组中的数据拷贝到新数组
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
res.append('[');
for(int i = 0 ; i < size ; i ++){
res.append(data[i]);
if(i != size - 1)
res.append(", ");
}
res.append(']');
return res.toString();
}
}
复杂度分析
最坏时间复杂度O(~)
大O描述的是算法的运行时间和输入数据之间的关系。
一般指渐近时间复杂度,描述n趋向于无穷的情况。
添加操作–O(n)
addLast(e)
——-O(1)
addFirst(e)
——O(n)
add(Index, e)
—-O(n/2)=O(n)
运用概率论的知识。index随机1-n概率均为1/n,
计算运行时间期望。
删除操作–O(n)
removeLast(e)
——-O(1)
removeFirst(e)
——O(n)
remove(Index)
—-O(n/2)=O(n)
修改操作–O(1)
set(index, e)
查找操作–O(1)
get(index)
contains(e)
find(e)
均摊时间复杂度
resizable(capacity)
虽然为O(n),但是不是每次都会执行到的。
对于一个容量为8的数组来说,前8次增加都不会调用resizable
,但是当第9次添加元素时,会调用一次resizable
,一次resizable
为8次操作。
总结地说,9次addLast操作(这里假设用的都是复杂度最低的addLast
),触发resizable
,总共进行了17次基本操作。
推广到n,也就是对于一个capacity=n的数组来说,进行n+1次操作,触发一次resizable
,总共进行2n+1次操作。
也就是说,平均每次addLast
操作,进行2次基本操作。
由此引入均摊复杂度(amortized time complexity–
addLast
的均摊复杂度为O(1),removeLast
同理。
复杂度震荡
对于容器为10的操作中,当进行第11次增加addLast
时,capacity增加到20,可是当我们在这之后又进行移除removeLast
操作的时候,capacity又将变成10。循环往复,频繁进行resizable
操作的话,这样就会造成不必要的性能浪费。
原因:removeLast
操作过于着急(Eager)
解决办法:Lazy。即增加的时候若容器capacity不够时,立刻增加capacity至原来的2倍。可是在减小容器capacity的时候,我们可以稍微lazy一点,当元素数量为capacity的1/4时我们才去减少capacity为原来的1/2。这样就可以有效解决震荡引起的性能损失。
笔记整理来源于liuyubobobo老师在慕课网的《玩转数据结构 从入门到进阶课程》