数据结构之数组
一、数组的定义:
数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识。
数组内的元素是连续存储的,所以数组中元素的地址,可以通过其索引计算出来。
知道了数组的数据起始地址
B
a
s
e
A
d
d
r
e
s
s
BaseAddress
BaseAddress,就可以由公式
B
a
s
e
A
d
d
r
e
s
s
+
i
∗
s
i
z
e
BaseAddress + i * size
BaseAddress+i∗size 计算出索引
i
i
i 元素的地址。
●
i
i
i 即索引,在 Java、C 等语言都是从 0 开始
●
s
i
z
e
size
size 是每个元素占用字节,例如
i
n
t
int
int 占
4
4
4,
d
o
u
b
l
e
double
double 占
8
8
8
数组的结构:
数组在java中实际上就是一个对象,分为对象头部分和数据部分,其中:
对象头部分
- markword :主要包括哈希码,垃圾回收时的分代年龄(如年轻代,老年代,),一些锁的信息。占用8个字节。
- 类指针(cp),主要用来分辨对象的类型,占用四个字节。
- 记录数组的大小,占用四个字节。
数据部分
数组元素 + 对齐字节 (java中的所有对象的大小都是8字节的整数倍,不足的部分要用对齐字节来补充)
时间复杂度
- 在知道查找数组的索引的时候,时间复杂度为O(1);
- 查询未知元素的时候必须遍历整个数组,消耗时间跟数组中的数据数量有关。
- 删除或者添加某个元素的时候,消耗相同的时间同样要根据数据规模判断。时间复杂度为O(n);
二、动态数组的实现(集合底层原理)
对于动态数组,其实就是JAVA中的集合的展现,在这里分别介绍动态数组的,添加元素遍历查询,插入,删除,与扩容的方法实现
首先我们需要创建一个Array的类内部来实现动态数组
内部定义三个私有属性如下:
private int size =0;//插入的数据逻辑大小
private int cap = 8;//数组容量
private int [] array = {};//数组的初始容量,先定义一个空数组,在我们使用它的时候再进行初始化。
1.添加元素
代码如下(示例):
第一种将新增的元素添加到数组的尾部:
public void addLast(int value){
array[size] = value;
size++;
}
第二种根据数组的索引添加元素:
public void add(int index,int value){
if (index >= 0 && index <size){
//将原数组在index后的数据进行拷贝。
System.arraycopy(array,index,array,index+1,size -index);
}
array[index] = value;
size++;
}
2.遍历数组中的数据
代码如下(示例):
第一种方式:
定义foreach方法,使用函数式接口cosumer来接受参数,使用cosumer的好处是没有返回值。用户便可以使用我们自定义的方法来传入参数得到数组中的每一个元素,并且可以对这些数据进行自定义的操作(写入数据库,遍历输出等等);
public void foreach(Consumer<Integer> consumer){
for (int i = 0; i < size ; i++) {
consumer.accept(array[i]);
}
}
第二种方式:
对于Array类实现迭代器Iterator接口,并重写其中的Iterator方法,在这个方法中用匿名内部类的方式来实现Iterator方法,创建子类实现其中的hasNext和next方法;使得我们的数组可以实现迭代器遍历输出;
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {//因为Iterator是一个接口,要想使用它就要实例化它,也就是说创建一个Iterator的子类实现这个接口;
int i = 0;
@Override//重写接口中的方法;
public boolean hasNext() {//询问有没有下一个元素
return i < size;
}
@Override
public Integer next() {
return array[i++];//++i:先自增,再使用i值,i++先使用i值再自增
}
};
}
第三种方式:
使用流的方式实现数组数据的迭代,将我们数组中的有效数据放入流中,再使用流的forEach方法进行输出。
public IntStream stream(){
//对原数组进行复制,提取有效元素复制头不复制尾
return IntStream.of(Arrays.copyOfRange(array,0,size));
}
3.删除数组中的数据
删除数组中的数组原理就是让现在的数组复制原来旧的数组删除索引之后的内容,删除节点的索引之前的数据进行保留,这样完成一个复制以后,便删除了想要删除的索引的数据。
public int remove(int index){
int remove = array[index];
if (index < size - 1)
//复制原有数组删除节点索引之后的内容,长度为有效数据-删除索引数据数据及其之前的数据
System.arraycopy(array,index+1,array,index,size - index - 1);
size--;
return remove;
}
4.动态数组的扩容
由于普通定义的数组一旦生成是无法改变其大小的,所以这也是我们动态数组的最大优点,就是可以根据需求动态的改变数组的大小。
数组的动态化初始:
在我们不使用数组的时候,数组为一个空数组,并不占用空间,当我们在java堆中创建Array类的对象时才会动态的在对象中开辟我们的数组空间。
代码如下:
第一种是不指定大小,则生成一个默认容量为8的数组;
private int size =0;
private int cap = 8;
private int [] array = {};
public Array(){
if (size == 0) {
array = new int[cap];
}
}
当然我们也可以选择自定义生成的数组的大小:
public Array(int cap) {
this.cap = cap;
if (size == 0) {
array = new int[cap];
}
当我们默认的数组大小,或者自定义的数组的数量满的时候,这时候我们应该对我们的数组进行一个扩容,基本原理就是,生成一个更大的数组将我们已经装满的数组进行复制,这样就可以装入更多的数组,而新数组的大小是自己定义的这里我们定义当原有数组大小不够用的时候就创建1.5倍的原数组来使用。
代码如下:
public void checkArray(){
// 容量检查
if (size == cap) {
//当原有数组的容量不够时,进行1.5倍扩容
cap += cap >> 1;
int[] newArray = new int[cap];
System.arraycopy(array, 0,
newArray, 0, size);
array = newArray;
}
}
三、二维数组
- 二维数组的定义
代码实例
int[][] array = {
{2,4,6,8},//索引0
{1,3,5,7},//索引1
{4,7,9,0},//索引2
其实二维数组就是存储了许多个一维数组,二维数组中存储着其内部的一维数组的地址,用户可以根据这些地址,使用索引进行调用从而找到里面的一维数组,再通过索引来获取一维数组里面的值。
扩展:数组的缓存与局部性原理
CPU的读取数据的速度是皮秒级的,内存读取数据的速度是纳米级的,而cpu想要处理数据就要先把数据放入内存,而后从内存中读取要处理的数据,处理完再放回到我们的内存中。为了加快读取的速度,这里引入一个新的概念就是缓存,
就好比我们播放一个电视剧,有没有注意到进度条后面有一个灰色的进度条,那一块就是我们的缓存数据,我们的CPU从内存中读取到数据在解析到播放器小号的时间是比较长的,如果网络还好的话可以忽略,但是如果你的网络很卡。只靠cpu去读取和解析数据就会非常的卡,这个时候缓存就非常的重要,在你的Cpu解析数据的时候,缓存就从内存中准备好数据供Cpu使用,内存的读取速度比较慢,但是缓存的速度很快接近于Cpu,这样就加快了视频的播放,提高了视频的流畅度。
那么缓存读取数据一般是以64字节进行计算得,称之为一个缓存行。
缓存从内存中读取数据的时候,会将读取的数据连同其周围的数据凑一个缓存行来读取,这个行为称之为空间局部性而当缓存中不存在我们需要的元素的时候CPU还是要去内存中寻找数据,这个时候就慢很多。
利用这个特性,在读取二维数组的时候尽量的使我们读取数组的时候是连续性的读取,也就是先读取行,再读取列,也就是先读取每个一维数组存放的地址再读取其中的数据。这样效率会高很多。