导读概述
提到数组,我相信你肯定不陌生,甚至还会自信地说,它很简单啊。是的,在每一种编程语言中,基本都会有数组这种数据类型。不过,它不仅仅是一种编程语言中的数据类型,还是一种最基础的数据结构。尽管数组看起来非常基础、简单,但是我估计很多人都并没有理解这个基础数据结构的精髓。比如有没有想过数据为什么从零开始查找,从1查找不更符合人类思维吗?
带着疑问,本文将从以下几个方面分享一下数组原理及应用
1.什么是线性表
2.什么是数组
3.数组下标为什么从0开始
4.数组的常规操作
5.小结,从本文中学到了什么
线性表
首先在讲解数组前先理解线性表和非线性表,因为数组属于线性结构的一种,这样从全局俯视一下数组对你认识数组更有帮助。
什么是线性表
线性表:(linear list)线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向,是一个有序数据元素的集合。
线性表包含:数组、链表、队列、栈等都是线性表结构。
图例如下:
什么是非线性表
非线性表:与线性表相对立的概念是非线性表。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。
非线性表:二叉树、堆、图等,图例如下:
数组
什么是数组
数组:数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
示意图:
数组原理
先来说一下数组的存储原理:
数组用一组连续的内存空间来存储一组具有相同类型的数据。
数组可以根据下标随机访问数据
比如一个整型数据 int[] 长度为5
-
假设首地址是:1000
-
int是4字节(32位),实际内存存储是位
-
随机元素寻址
//上图详细公试:
a[i]_address=a[0]_address+i*4
//通用公式:
//base_address首地址相当于a[0],data_type_size 表示数组中每个元素的大小 案例4个字节
a[i]_address = base_address + i * data_type_size
base_address首地址相当于a[0]。
data_type_size 表示数组中每个元素的大小,假设4个字节。
该公式解释了三个方面
-
连续性分配
-
相同的类型
-
下标从0开始
数组操作
读取元素
根据下标读取元素的方式叫作随机读取
案例:int n=nums[3]
/**
* 获取元素
* @param index
* @return
*/
public int get(int index){
return nums[index];
}
更新元素
案例:nums[3]= 8;
/**
* 通过数组下标更新数组元素
* @param index
* @param n
*/
public void update(int index,int n){
nums[index] = n;
}
注意不要数组越界
读取和更新都可以随机访问,时间复杂度为O(1)
插入元素 有三种情况:
尾部插入
在数据的实际元素数量小于数组长度的情况下:
直接把插入的元素放在数组尾部的空闲位置即可,等同于更新元素的操作
尾部插入的时间复杂度为O(1)
案例:nums[6]= 12;
/**
* 尾部插入
* @param n
*/
public void insertTail(int n){
nums[nums.length]=n;
}
中间插入
在数据的实际元素数量小于数组长度的情况下:
由于数组的每一个元素都有其固定下标,所以首先把插入位置及后面的元素向后移动,腾出地方,再把要插入的元素放到对应的数组位置上。
中间插入或头部插入的时间复杂度为O(n)
/**
* 数组中间插入
* @param point
* @param n
*/
public void insertMid(int point,int n){
for (int i=0;i<nums.length;i++){
if(nums[i]!=0){
nums[i+1]=nums[i];
}
}
nums[point-1]= n;
}
范围插入
假如现在有一个数组,已经装满了元素,这时还想插入一个新元素,或者插入位置是越界的,这时就要对原数组进行扩容:可以创建一个新数组,长度是旧数组的2倍,再把旧数组中的元素统统复制过去,这样就实现了数组的扩容。
/**
* 数组copy
*/
public void resize(){
int[] newNums = new int[nums.length*2];
System.arraycopy(nums,0,newNums,0,nums.length);
nums = newNums;
}
/**
* 数组扩容
* @param point
* @param n
*/
public void insertOutOfBounds(int point,int n){
//数组扩容
resize();
nums[point-1]=n;
}
删除元素
数组的删除操作和插入操作的过程相反,如果删除的元素位于数组中间,其后的元素都需要向前挪
动1位。
尾部删除最好的时候复杂度O(1)
头部删除或中间删除都需要移动元素时间复杂度O(n)
for(int i=0;i<nums.length;i++)
{
nums[i-1]=nums[i];
}
完整代码:
/** 数组实现 查询 中间插入,尾部插入,数组扩容
* @author lihaoran
*/
public class ArrayDemo {
int[] nums = new int[8];
/**
* 获取元素
* @param index
* @return
*/
public int get(int index){
return nums[index];
}
/**
* 通过数组下标更新数组元素
* @param index
* @param n
*/
public void update(int index,int n){
nums[index] = n;
}
/**
* 尾部插入
* @param n
*/
public void insertTail(int n){
nums[nums.length]=n;
}
/**
* 数组中间插入
* @param point
* @param n
*/
public void insertMid(int point,int n){
for (int i=0;i<nums.length;i++){
if(nums[i]!=0){
nums[i+1]=nums[i];
}
}
nums[point-1]= n;
}
/**
* 数组copy
*/
public void resize(){
int[] newNums = new int[nums.length*2];
System.arraycopy(nums,0,newNums,0,nums.length);
nums = newNums;
}
/**
* 数组扩容
* @param point
* @param n
*/
public void insertOutOfBounds(int point,int n){
//数组扩容
resize();
nums[point-1]=n;
}
/**
* 删除指定下标值
* @param point
*/
public void deleteMid(int point){
for (int i=point;i<nums.length;i++){
nums[i-1]=nums[i];
}
}
public void display(){
for(int n:nums){
System.out.println(n);
}
}
}
学到了什么
数组如何实现的随机访问?
数组连续的内存空间和相同类型的数据。正是因为这两个特性,从而才使得数组有随机访问的能力;
缺点:这两个特性也让数组的很多操作变得非常低效,比如要想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。
补充:随机访问的意思,就是可以随机选择下标,进行数据访问。
为什么很多编程语言的数组都是从0开始编号的Why?
原因一:减少CPU指令运算
-
下标从0开始:
数组寻址
a[i]_address = base_address + i * type_size
其中base_address为数组arr首地址,arr0就是偏移量为0的数组,即数组arr首地址;i为偏移量,type_size为数组类型字节数,比如int为32位,即4个字节。
-
下标从1开始:
数组寻址
a[i]_address = base_address + (i-1)*type_size
比较两个计算公式可以发现公式(2)每次CPU寻址需要多一次 i-1的操作,对于CPU即多了一次减法的指令运算。
对于数组这种基础数据结构,无论在哪种高级程序语言中,都是频繁间接(作为容器的基础数据结构,比如Java的ArrayList)或者直接被使用的,因此要尽量减少其消耗CPU资源。
原因二:历史原因
-
语言出现顺序从早到晚C、Java、JavaScript。
-
C语言数组下标是从0开始->Java也是->JavaScript也是。
-
低额外的学习和理解成本。
原因三:物理内存的地址是从0开始的
计算机主存是多个连续字节大小的单元组成的数组,每个字节都对应唯一的物理地址,第一个字节的地址为0。
数组的操作复杂度分析
随机访问时间复杂度O(1)
插入
若有一元素想往int[n]的第i个位置插入数据,需要在i-n的位置往后移。
-
最好情况时间复杂度 O(1)
-
最坏情况复杂度为O(n)
-
平均负责度为O(n)
补充:如果数组中的数据不是有序的,也就是无规律的情况下,可以直接把第i个位置上的数据移到最后,然后将插入的数据直接放在第i个位置上。这样时间复杂度就降为 O(1)了。
删除:
与插入类似,为了保持内存的连续性。
-
最好情况时间复杂度 O(1)
-
最坏情况复杂度为O(n)
-
平均负责度为O(n)
数组和链表有什么区别?
-
数组中的元素存在一个连续的内存空间中,而链表中的元素可以不存在于连续的内存空间。
-
数组支持随机访问,根据下标随机访问的时间复杂度是O(1);链表适合插入、删除操作,时间复杂度为O(1)。
• 后记 •
通过本章数组分析,希望你对数组有了重新的认识了。本套系列持续更新中,已收录到【算法】和【数据结构】专栏里(干货满满),如果感觉有所收获,可以动动小手指给点个赞,感谢阅读!
扫码关注微信公共账号!!
作者微信公共账号:算法5分钟|浅入浅出【数组】