1、什么是数组
数组对应的英文是 array,是有限个相同类型的变量所组成的有序集合,数组中 的每一个变量被称为元素。数组是最为简单、最为常用的数据结构;以整型数组为例,数组的存储形式如下图所示:
数组中的每一个元素有着自己的下标,只不过这个下标从0开始,一直到数组长度-1;数组的另一个特点,是在内存中顺序存储,因此可以很好地实现逻辑上的顺序表。
数组在内存中的顺序存储:内存是由一个个连续的内存单元组成的,每一个内存单元都有自己的地址。在 这些内存单元中,有些被其他数据占用了,有些是空闲的;
数组中的每一个元素,都存储在小小的内存单元中,并且元素之间紧密排列, 既不能打乱元素的存储顺序,也不能跳过某个存储单元进行存储。
如图,橙色的格子代表空闲的存储单元,灰色的格子代表已占用的存储单元,而红色的连续格子代表数组在内存中的位置。不同类型的数组,每个元素所占的字节个数也不同。
2、数组的基本操作
【1】读取元素
对于数组来说,读取元素是最简单的操作。由于数组在内存中顺序存储,所以只要给出一个数组下标,就可以读取到对应的数组元素。
假设有一个名称为array的数组,要读取数组下标为3的元素,就写作 array[3];读取数组下标为5的元素,就写作array[5]。需要注意的是,输入的下 标必须在数组的长度范围之内,否则会出现数组越界,像这种根据下标读取元素的方式叫作 随机读取,简单的代码示例如下:
int[] array = new int[]{3,1,2,5,4,9,7,2};
// 输出数组中下标为3的元素
System.out.println(array[3]);
【2】 更新元素
要把数组中某一个元素的值替换为一个新值,直接利用数组下标就可以把新值赋给该元素,代码示例如下:
int[] array = new int[]{3,1,2,5,4,9,7,2};
// 给数组下标为5的元素赋值
array[5] = 10;
// 输出数组中下标为5的元素
System.out.println(array[5]);
数组读取元素和更新元素的时间复杂度都是O(1)
【3】 插入元素
数组的实际 元素数量有可能小于数组的长度,例如下面的情形:
因此,插入数组元素的操作存在3种情况:
- 尾部插入
尾部插入,是最简单的情况,直接把插入的元素放在数组尾部的空闲位置即可,等同于更新元素的操作
- 中间插入
由于数组的每一个元素都有其固定下标,所以不得不首先把插入位置及后面的元素向后移动,腾出地方,再把要插入的元素放到对应的数组位置上;
中间插入操作的完整实现代码如下:
public class MyArray {
private int[] array;
private int size;
public MyArray(int capacity){
this.array = new int[capacity];
size = 0;
}
// 数组插入元素
public void insert(int element, int index) throws Exception {
//判断访问下标是否超出范围
if(index<0 || index>size){
throw new IndexOutOfBoundsException("超出数组实际元素范围!");
}
//从右向左循环,将元素逐个向右挪1位
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.insert(7,1);
myArray.insert(9,2);
myArray.insert(5,3);
myArray.insert(6,1);
myArray.output();
}
}
代码中的成员变量size是数组实际元素的数量,如果插入元素在数组尾部,传入的下标参数index等于size;如果插入元素在数组中间或头部,则index小于 size;
如果传入的下标参数index大于size或小于0,则认为是非法输入,会直接抛出异常
- 超范围插入
假如现在有一个长度为6的数组,已经装满了元素,这时还想插入一个新元素:
上述操作涉及数组的扩容,可以创建一个新数组,长度是旧数组的2倍,再把旧数组中的元素统统复制 过去,这样就实现了数组的扩容:
插入元素方法需要改写,改写后的代码如下:
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("超出数组实际元素范围!");
}
// 如果实际元素达到数组容量上限,则对数组进行扩容
if(size >= array.length){
resize();
}
//从右向左循环,将元素逐个向右挪1位
for(int i=size-1; i>=index; i--){
array[i+1] = array[i];
}
//腾出的位置放入新元素
array[index] = element;
size++;
}
//数组扩容
public void resize(){
int[] arrayNew = new int[array.length*2];
// 从旧数组复制到新数组
System.arraycopy(array,0,arrayNew,0,array.length);
array = arrayNew;
}
// 输出数组
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.insert(7,1);
myArray.insert(9,2);
myArray.insert(5,3);
myArray.insert(6,1);
myArray.output();
}
}
输出:
【4】 删除元素
数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪动1位:
删除操作代码如下:
public class delete {
private int[] array;
private int size;
public int delete(int index) throws Exception{
// 判断访问下标是否超出范围
if(index<0 || index>=size){
throw new IndexOutOfBoundsException("超出数组实际元素范围!");
}
int deletedElement = array[index];
//从左向右循环,将元素逐个向右挪1位
for(int i=index; i<size-1; i++){
array[i] = array[i+1];
}
size--;
return deletedElement;
}
}
删除操作,只涉及元素的移动,时间复杂度是O(n)
如果对数组元素没有顺序要求,删除操作还存在一种取巧的方法:
如下图所示,需要删除的是数组中的元素2,可以把最后一个元素复制到元素2所在的位置,然后再删除掉最后一个元素
这样一来,无须进行大量的元素移动,时间复杂度降低为O(1)。
3、数组的优势和劣势
优势:
数组拥有非常高效的随机访问能力,只要给出下标,就可以用常量时间找到对应元素。有一种高效查找元素的算法叫作二分查找, 就是利用了数组的这个优势。
劣势:
数组的劣势体现在插入和删除元素方面。由于数组元素连续紧密地存储在内存中,插入、删除元素都会导致大量元素被迫移 动,影响效率。
总结:
数组所适合的是读操作多、写操作少的场景!
—————————————————————————————————————————
内容来源:《漫画算法》