定义:
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
该定义有几个关键点:
1.线性表。
顾名思义,线性表就是数据排成像一条线一样的结构。这种结构具有下列特点:存在一个唯一的没有前驱的(头)数据元素;存在一个唯一的没有后继的(尾)数据元素;此外,每一个数据元素均有一个直接前驱和一个直接后继数据元素。
常见的线性表还有链表、队列、栈等等.
2.一组连续的内存空间和相同类型的数据。
连续内存空间,说明在声明数组时需要预先指定大小。
相同类型数据很简单,这里和容器作区分,更容易了解,以java ArrayList容器为例,首先数组只能存储基本类型,比如int、long,ArrayList则需要在进一步,使用基本类型的包装类,存储Integer,Long。这里要提一下Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
这也就是说,存储数组时,对内存要求比较高,这就涉及到内存的分配了。假设声明一个1M大小的数组时,也就说不能是零散的内存块不能连接成一个大的空间,而必须要一整块连续的内存空间才能申请成功。
因为在申请和释放许多小的块可能会产生如下状态:在已用块之间存在很多小的空闲块。进而申请大块内存失败,虽然空闲块的总和足够,但是空闲的小块是零散的,不能满足申请的大小。
特性:随机访问。
优点:数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
说到随机访问,提一下寻址公式:
a[i]_address = base_address + i * data_type_size
对于 m * n 的二维数组,a [ i ][ j ] (i < m,j < n)的地址为:
address = base_address + ( i * n + j) * type_size
(data_type_size 表示数组中每个元素的大小)
在这里在说一下寻址公式。以一个int[10]的数组为例,计算机给数组分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000,因为存的是int型,这里data_type_size便是4个字节。
套入寻址公式可得 a[0] 对应 1000~1003,a[1]对应1004~1007,,,以此类推。
缺点:增删低效,因为数组中的数据是有序的,每一次的增删(除了头部和尾部)都伴随着数组的移动。
简单罗列一下时间复杂度:
1) 插入:最好O(1) 最坏O(n) 平均O(n)
小技巧:数组若无序,插入新的元素时,可以将第K个位置元素移动到数组末尾,把心的元素,插入到第k个位置,此处复杂度为O(1)。
3) 删除:最好O(1) 最坏O(n) 平均O(n)
小技巧:多次删除集中在一起,提高删除效率(JVM 标记清除垃圾回收算法的核心思想)。简单地说,就是每次的删除不是真正的删除,而是添加标记,什么时候等存满了,统一集中在一起删。
注意:
下标越界
一定要主要数组的下标是从0开始,一定要注意最大的访问值是size-1。
在 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。根据我们前面讲的数组寻址公式,a[6] 也会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,那么 a[6]=0 就相当于 i=0,所以就会导致代码无限循环。
数组越界在 C 语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因为,访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。
JAVA本身还好,自身会做越界检查,如果越界则会抛出 java.lang.ArrayIndexOutOfBoundsException
疑问:
数组和容器之间有什么区别,哪个更好一些?
以Java ArrayList为例:
ArrayList是Java集合框架类的一员,可以称它为一个动态数组。它还有一个优势就是将数组的操作封装起来,比如增删查,以及动态扩容。
重点来了,动态扩容:
当大小为n的数组此时已经存了n个数据,当n+1的数据需要存储的时候,数组就需要重新分配一块更大的连续的内存,然后将之前的数据复制过去,然后再进行操作,费时费力。
如果使用ArrayList的话,就完全不需要考虑底层了,因为它已经将底层扩容逻辑封装好了,当空间不够的时候,它会自动扩充为原来的1.5倍大小。
总结:对于业务开发,直接使用容器就足够了,省时省力。虽然损耗一丢丢性能,但是完全不会影响到系统整体的性能。但如果做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。
end
您的点赞和关注是对我最大的支持,谢谢!