02数组
什么是数组
数组是一种线性结构,用一组连续的内存空间来储存一组相同数据类型相同的数据。
线性结构
我线性结构就是像一条线一样,只有前后两个方向(或者只有一个方向),线性结构除了数组还有链表、队列、栈等结构,
而非线性表则像图、二叉树、堆之类的。
连续内存空间和相同数据类型
数组的连续的内存空间正如图一所示,在一个个小小的内存单元中,一组数据的每一个元素都是必须紧密排列的,中间不允许有空格打乱。以及这一组的数组数据类型必须相同。
数组有一个最大的特点。即支持随机访问(即可根据下标任意访问一个数组元素)
例如图二,这是一个长度为10的数组a,下标从0开始到9,分配了一组连续的内存空间,空间地址从1000开始到1039结束。即下标为0的首元素地址为1000,因为是int数组,每一个元素为4个字节,所以a[0]的地址是1000-1003,a[1]地址为1004-1007,一次类推。
由此推出计算机根据下标访问数组的某个元素时,寻址公式为:
##寻址公式
a[i]_address = base_address + i * data_type_size
数组的一个特点
链表适合插入、删除、这些操作时间复杂度是o(1),
数组支持随机访问,根据下标查找的时间复杂度为o(1),根据二分查找的话是o(logn)
数组的几个操作
数组基本操作,读取查找、更新、插入、删除
读取元素
数组查找的话又称随机访问,根据下标和寻址公式快速查找一个元素(下标要在数组长度范围内,否则会出现数组越界)。
##随机访问
int[] array = new int[]{
1,2,3};
更新元素
数组更新一个元素的话也是找到元素地址进行赋值
##更新元素
int[] array = new int[]{
1,2,3};
array[0] = 111;
插入元素
插入元素分别三种情况,中间插入和尾部插入以及超范围插入
尾部插入
尾部插入最简单,直接在数组末尾空余位置插入即可,时间复杂度为o(1)
中间插入
插入(低效)
数组每个元素都有固定的下标,所以要想插入一个元素,那么这个插入位置原本及后面的元素都要往后退移一个位置来腾出位置给插入元素。
#中间插入代码
public class MyArray {
private int[] array; //数组
private int size; //数组内实际元素的长度
public MyArray(int capacity){
this.array = new int[capacity];
size = 0;
}
//插入元素
//element 插入元素
//index 插入位置
public void insert(int element, int index) throws Exception{
//判断插入元素是否在符合范围
if(index<0 || index >size){
throw new IndexOutOfBoundsException("数组越界");
}
//插入位置元素和后面元素都后挪一个位置
for (int i = size -1; i>= index; i--){
array[i+1] = array[i];
}
//空出来的的位置插入元素
array[index] = element;
size++;
}
public void output(){
for (int i=0;i<size;i++){
System.out.println(array[i]);
}
}
public static void main(String[] args)throws Exception{
MyArray myArray =new MyArray(10);
myArray.insert(3,0);
myArray.output();
}
}
高效的插入方法
如图所示,当我们数组储存的数组没有顺序要求时,当我们要在k下标插入一个元素,我们可以在数组末尾把这个元素插入在末尾,然后将位置k的值赋值成要插入的元素值即可
超范围插入(数组扩容)
数组的长度在一开始创建就确定后的,后面当数组满了后若还想继续插入元素,可以进行一个数组扩容插入。
public class MyArray1 {
private int[] array; //数组
private int size; //数组内实际元素的长度
public MyArray1(int capacity){
this.array = new int[capacity];
size = 0;
}
//插入元素
//element 插入元素
//index 插入位置
public void insert(int element, int index) throws Exception{
//判断插入元素是否在符合范围
if(index<0 || index >size){
throw new IndexOutOfBoundsException("数组越界");
}
if(size>=array.length){
resize();
}
//插入位置元素和后面元素都后挪一个位置
for (int i = size -1; i>= index; i--){
array[i+1] = array[i];
}
//空出来的的位置插入元素
array[index] = element;
size++;
}
public void output(){
for (int i=0;i<size;i++){
System.out.println(array[i]);
}
}
public void resize(){
int[] arraynew = new int[]{array.length*2};
System.arraycopy(array,0,arraynew,0,array.length);
array = array;
}
public static void main(String[] args)throws Exception{
MyArray1 myArray =new MyArray1(10);
myArray.insert(3,0);
myArray.output();
}
}
删除元素
删除操作跟插入操作一样,当你删除一个元素后,后面的元素都得往前进一个内存单元。
#这是一个普通删除的操作
public int delete(int index) throw Exception{
if(index <0 || index >= size){
throw new IndexOutOfBoundsException("越界了噢 ");}
int deletedElement = array[index];
for(int i =index;i<size -1; i++){
array[i] = array[i+1];}
size --;
return deletedElement;
}
删除元素(无顺序要求)
如果数组中的元素是无顺序要求的,那么可以有一种时间复杂度比较低的删除方法,就是先将最后一个元素复制到要删除的元素,这样要求删除的元素就会被覆盖性删除,此时我们只需要删除最后一个元素即可,这样就避免了大量元素往前移动的不必要,详细代码就不写了,这里只记录一个思想。
删除元素(标记延迟删除法( GC))
每一次进行普通的删除,数组就得大规模移动数据一次,那么是不是有一种方法,当我们要删除一个元素的时候,我们只是对这个元素进行一次标记,逻辑上判断是这个元素被删除了。当 我们内存满了后再真正进行一次性的删除,这样也能避免频繁的删除导致大量元素移动。
容器和数组
对数组类型,很多语言会提供一个容器类,例如Java中的ArrayList, ArrayList最大的优势是将很多数组的操作细节封装起来,例如删除添加插入等,另外arraylist还有一个优势就是支持动态扩容。当使用arraylist的时候数组空间不足,array list会自动将空间扩大为1.5倍(当然,申请时间也是耗时的,因为涉及了内存申请和数据迁移,所以还是一开始先指定数组大小)。
容器的几个特性
- Java中arraylist无法储存基本类型,比如int,long,需要封装为integer,Long类型,而Autoboxing和Unboxing会消耗一定的性能,如果关注性能或者基本类型的话可以使用数组。
- 如果数组大小事先已知的话,而且操作简单用不到arraylist的大部分方法,可以直接使用数组
为什么数组下标要从0开始
#例如我们要求a[k]的内存地址,假设我们从1开始
#那么根据寻址公式有
a[k]_address = base_address+(k-1)*type_size
这样一看,如果是从1开始算起,那么当我们使用数组计算机运行代码时,就会给CPU多下达了一道指令
k-1,数组的运行效率达不到最优,
对于二维数组的寻址公式
对于一个m*n的二维数组a[i][j]
a[i][j]_adrress = base_address +( i*n +j) * type_address