这篇文章,我们来介绍一下第一个数据结构——数组
目录
1.概述
1.1定义
1.2 性能
2.动态数组
3.动态数组的实现
4.二维数组
5.合并两个数组
6.总结
在java基础部分,我们已经介绍过数组,那时候介绍的数组侧重于介绍数组的创建与使用,在涉及到底层的方面上,讲述的比较少,而这里再次介绍数组将侧重于在底层方面上介绍数组的一些特点。
数组的定义:在计算机科学中,数组是由一组元素(值或变量)组成的数据结构,每个元素有至少一个索引或键来标识
注意:
数组中的元素在内存中是连续存储的,索引数组中元素的地址,可以通过其索引计算出来。比如我们知道了数组的数据的起始位置为BaseAddress,可以由公式BaseAddress+i*size计算出索引 i 元素的地址(i 即索引,在java、c等语言都是从0开始的;size是每个元素占用的字节,例如 int 占用4,double占8)
下面来分析一下一个数组的空间占用情况
java中数组的结构为:
举例说明:int[ ] array = {1,2,3,4,5}
空间占用大小为40字节,组成如下:8+4+4+5*4+4=40
解释:
我们看一下下面的这张图:
随机访问
因为数组的根据索引来查找元素的,所以数组访问的时间复杂度为O(1)
我们前面所学的和所使用的数组都是静态的数组,一经创建,它的容量是固定的,无法改变,下面我们来学习一下动态数组
首先,我们来思考一下动态数组是什么样的。
首先,会给你一个数组,它里面是空的,你可以往里面放东西,也可以删除内容,也可以插入内容。等到它装满了,我们还要装,那这个数组就要扩容,假设新容量是原乡容量的两倍,然后将原数组中的内容拷贝到新数组中,然后就可以继续添加内容了。
下面,思考一下应该怎么实现。
首先,肯定有一个数组作为基本数组,然后有一个变量来记录数组里面有效值的个数。最开始的时候,我们没有添加值,所以有效值为0,然后我们往数组里面添加值,在添加的时候我们要判断有效值的大小是否小于数组的大小,如果小,则可以添加,如果大,则扩容拷贝再添加,扩容拷贝java中有方法。删除的时候要判断删除的位置是否是尾位置,如果是尾位置,则直接令有效值减1,如果要删除的元素在中间,则先要将后面的元素都往前面挪一位,然后再令有效值减1,更改和查看都是直接根据索引来的,遍历直接一个for循环就OK了,插入和添加的逻辑相似,只不过多了一步拷贝挪位的过程。
OK,思路有了,下面来实现一下动态数组
下面来看一下动态数组的实现
具体的讲解可以看上面的思路和代码中的注释
下面给出具体的代码:
import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.stream.IntStream;
/**
* 动态数组
* */
public class L1_DynamicArray implements Iterable{
//定义的基础数组,这是一种懒定义模式,即一开始不给出数组的长度
private int[] array = {};
//定义变量,记录数组的有效值
private int size = 0;
//在数组某位添加元素的方法,变量为要添加的值
public void addLast(int element){
// 就直接让size出的值等于要添加的值
// array[size] = element;
// 然后size++就可以了
// size++;
//这里整合了代码,直接调用下面的方法,在末尾添加元素本质上就是在size处添加元素
add(size,element);
}
//在目标索引index出添加元素element
public void add(int index,int element){
//扩容逻辑:
checkAndGrow();
//下面是添加逻辑:
//首先判断index是否符合要求,符合要求,OK,index后面的元素挪位置
if(index >= 0 && index <= size){
/**这个方法的参数的含义如下:
* 第一次参数:你要复制那个数组中的元素
* 第二个参数:你要复制那个数组中的元素的起始位置
* 第三个参数:你要复制到哪个数组中,即复制的数据最终放哪
* 第四个参数:移动到的目标的起始位置是哪里
* 第五个参数:要移动多少个参数?
* */
System.arraycopy(array,index,array,index+1,size-index);
//赋值操作
array[index] = element;
//有效值+1
size++;
}else {
System.out.println("索引出错");
}
}
private void checkAndGrow(){
if(size == 0){
array = new int[2];
}else if(size == array.length){
int L = array.length * 2;
int[] newArray = new int[L];
System.arraycopy(array,0,newArray,0,size);
array = newArray;
}
}
//获取index处的元素
public int get (int index){
return array[index];
}
//最简单最基础最死板的遍历
public void forI(){
for (int i = 0; i < size; i++) {
System.out.println(array[i]);
}
}
/**
* 上面的遍历是写死的,只实现了打印的功能,但是有时我们可能需要这个元素去做别的事情,所以这里的遍历最好不要写死
* 所以我们就使用函数是接口来写
* 使用函数式接口的时候我们需要考虑两个问题:我们能给这个接口传递什么?我们需要从这个接口得到什么?
* 根据上面的两个问题,我们使用consumer这个接口
*/
public void foreach(Consumer consumer){
for (int i = 0; i < size; i++) {
consumer.accept(array[i]);
}
}
//迭代器遍历
//使用迭代器遍历最常用的方法就是实现一个接口,然后实现接口里面的方法
@Override
public Iterator iterator() {
return new Iterator() {
/**下面两个方法的使用场景一般是:
* 在一个循环中不断的调用hasNext方法,如果有那就不断循环,如果没有下一个元素,那就退出循环
* 然后在循环内部再不断的调用next方法
*/
//定义游标 i
int i = 0;
@Override
//遍历着去询问有没有下一个元素,有就返回true,没有就返回false
public boolean hasNext() {
//当i小于size时,表示有下一个元素,那就返回真
return i < size;
}
@Override
//返回当前的元素,并将指针移动到下一个元素
public Integer next() {
//返回当前的元素i,然后游标后移
return array[i++];
}
};
}
//使用流的方式对其进行遍历
public IntStream stream(){
return IntStream.of(Arrays.copyOfRange(array,0,size));
}
//删除的方法
public int remove(int index){
//用变量来接收要删除的元素
int removed = array[index];
//元素移位操作,这里对index的位置没有进行判断,是省略了,加上一个if判断一下也是可以的
System.arraycopy(array,index+1,array,index,size-index-1);
//有效值--
size--;
//返回要删除的元素
return removed;
}
/**
* 性能:
* 头部插入或删除:O(n)
* 中间插入或删除:O(n)
* 尾部插入或删除:O(1)(均摊来说)
* */
}
下面给出测试代码
测试结果就不展示了
先给出二维数组的语法:
然后能够弄清索引就行了。至于二维数组的内存情况,这个会分析对象的内存情况就会分析这个的内存情况。
算了,还是说一下吧
二维数组的本质还是多个一维数组。我们知道数组是一个对象,是对象就有其地址值。所以我们二维数组中实际存的就是每个一维数组的地址值,注意这些地址值可能是不连续的,但是这些地址值的地址值是连续的,然后那些可能不连续的地址值又标识着一些一维数组,这样就构成了一个二维数组。这就是二维数组在内存中的本质。
下面看一下如何合并两个数组:
这个其实并不难,看代码应该可以看懂
这篇文章主要讲了数组,重点侧重于动态数组的一些操作,其实都是很简单的内容,不复杂。