关注公众号:EZ大数据(ID:EZ_DATA)。每天进步一点点,感觉很爽!
ok,开始今天的表演,今天的主角就是数组Array。
大家对数组有什么看法呢?在我看来,数组其实就是把数据码成一排进行存放。
数组的最大优点就是:快速查询。在实际的工作场景中,数组最好应用于索引有语意的情况,为啥呢?因为我们可以很清楚的知道自己在查什么。当然,也并非所有有语意的索引都适用于数组,比如身份证号等,因为开辟的空间太大,用数组来查询,岂不是很浪费资源。
说到这里呢,肯定有小伙伴说,那数组可以处理“索引没有语意”的情况呢?当然可以!!!请大家思考,当索引没有语意时,如何表示数组内没有元素?如何添加元素?如何删除元素?
其实在Java中,我们使用的数组是没有这些功能的,这样的数组是静态数组,那么今天呢,我们就基于Java为我们提供的数组,来自己实现一个属于我们自己的数组类。
那么既然要写自己的数组类,就直接一步到位,我想让这个数组类,可以直接实现泛型,也就是不管什么类型,都可以在我们的数组里生存。
说到这里呢,再扯一点,Java的八种基本数据类型:boolean、char、byte、short、int、long、float、double是不能够直接存入的,但是每个基本数据类型对应的包装类:Boolean、Char、Byte、Short、Int、Long、Float、Double是可以滴。
好了,不说那么多,直接搞起代码,为了大家看的爽,我就直接贴上最实用的代码:
// 参数:capacity:数组中最多装多少个元素,size:实际中有多少个元素,在增删的操作中,需要维护size的值。
// 在第index个位置插入一个新元素e
public void add(int index, E e) {
if (size == data.length) {
throw new IllegalArgumentException("Add failed. Array is full");
}
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed. Require index >= 0 and index<=size");
}
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
那么这个方法就是直接实现了,数组在index的位置添加元素的功能。适用于数组开头和结尾的操作。
代码解析如下:
在index位置添加元素,那么index后面的元素都向后移一位,也就是data[i + 1] = data[i],同时呢,index位置的元素即赋值为添加的元素e,然后数组本身的size++操作即可。
删除操作代码如下:
// 从数组中删除index位置的元素,返回删除的元素
public E remove(int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Remove failed.Index is illegal");
}
E ret = data[index];
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
data[size] = null;
return ret;
}
代码解析如下:
在index位置删除元素,其原理与添加元素类似,即index位置后的元素全部向前移动一位,然后数组的size--操作即可。同时考虑到GC机制,对size位置的元素赋值为null。
ok,对于数组的增删查改来说,增删相对而言最为复杂,那么就先到这里。
下面我们来说说动态数组。
Java本身为我们提供的是静态数组,也就是说我在对数组进行增删操作的时候,其容量是不变的,那么我们来修改现在的代码,使得我们的数组容量capacity是可伸缩的。
具体的示意图如下:
假如我们数组里有四个元素,当插入第5个元素时,可以再创建一个新的数组,进行倍数扩容,然后呢,把当前数组里的每个元素,按索引都放置在新的数组中,同时数组指向新的数组,这样就可以实现动态数组。
好了,原理讲完,我们来个看代码:
// 定义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;
}
// 动态数组删除元素,当size为1/4时,容量变为1/2
public E remove(int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Remove failed.Index is illegal");
}
E ret = data[index];
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
size--;
data[size] = null;
if (size == data.length / 4 && data.length / 2 != 0) {
resize(data.length / 2);
}
return ret;
}
代码解析:
我们先实现一个resize方法,把原数组内的每个元素按索引,都迁移至新的数组内,即可实现动态数组。现在呢,就偷个懒,不放添加代码了,小伙伴们,可以自行实现。很EZ!
这里呢,就先卖个关子,为啥我取1/4,而不是数组容量的1/2?
好吧好吧,我也放上数组查和改的代码,供大家参考:
查找元素,代码如下:
// 查找数组中是否有元素e
public boolean contains(E e) {
for (int i = 0; i < size; i++) {
if (data[i].equals(e)) {
return true;
}
}
return false;
}
删除元素,代码如下:
// 修改index索引位置元素为e
public void set(int index, E e) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Get failed. Index is illegal");
}
data[index] = e;
}
当然,可以实现获取index索引位置的元素,查找元素e所在的索引,等等我就不再展示了,毕竟很EZ嘛。
重点来了,上篇文章刚说过时间复杂度。那么我们就来分析一波,我们写的这个动态数组增删查改的时间复杂度是多少呢?
对于添加操作来说,addLast(e) O(1),addFirst(e) O(n),add(index, e) O(n),resize O(n);
对于删除操作来说,removeLast(e) O(1),removeFirst(e) O(n),remove(index, e) O(n),resize O(n);
对于修改操作来说,set(index, e) O(1);
对于查询操作来说,get(index) O(1),contains(e) O(n),find(e) O(n)。
汇总来看呢,就是说:
增:O(n)
删:O(n)
查:已知索引O(1),未知索引O(n)
改:已知索引O(1),未知索引O(n)
那么,之前的问题,为啥删除操作时,我数组size变为1/4时,容量变为1/2。因为如果size变为1/2,那么此时无论增加还是删除元素,数组的容量一直在不停的变化。所以,当我删除元素时,设定为当size变为1/4时,然后再将数组动态变为1/2。
同时,还有一个问题,对于增删来说,最后一个元素进行操作,是O(1),但为何复杂度是O(n)呢,因为当动态数组变化时,resize操作的复杂度为O(n),需要把所有元素复制一遍到新的数组中。
额……,好难啊,感觉公众号写文章比博客难多了,不过今天的速度比昨天快不少,也算是小小的进步。
今天关于数组的文章,写的也挺乱的,不过我觉得吧,重要的东西已经说出来了,后续等我把代码同步到github上,方面各位小伙伴参考。
好了,下班下班,今天结束。收获不少,嘿嘿嘿……
拜了个拜!