深入理解数组Array
数组定义
数组(Array) :是有序1的元素序列。 若将有限2个类型相同3的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标,也叫索引4。
当你刚开始接触数组时或许会有以下疑问:
- 为什么数组是有序的;
- 必须是有限个;
- 数组是如何高效地随机访问?数组地址是如何计算的?首地址存放在哪?为什么必须是相同的元素?为什么索引从0开始?
- 低效的“插入”和“删除”。
- 如何对数组进行扩容。
- 什么是偏移量,什么是索引。
上述问题我们会在下面一一解答。
数组的优缺点
a. 优点:存取速度快。(数组直接按照地址进行存取)
b. 缺点:
- 需要一个连续的很大的内存(因为数组具有连续性,假设int[30000]arry 即需要分配连续的30000个空间,可能不存在)
- 插入和删除元素的效率低(当删除数组当中的一个元素时,因为数组具有连续性,所以需要后面的元素全部前移一位。同理:插入一位元素时,需要后面的全部后移一位)
一、为什么数组是有序的
初始化一个数组的时候,jvm会在内存上分配一块连续的内存空间,每一个内存空间存一个元素,从首地址开始连续存放,所以数组是有序的。如下图所示:
右图可以看出数组的结构在内存空间上是连续的,内存地址也按照一定的规律顺序排序(下面讲有什么样的规律)每一块内存空间存放一个元素,内存空间块在整个数组的内存空间中的顺序就是数据的索引(下标)。
二、为什么数组必须是有限个
由于数组的有序性,所以数组在初始化的时候,会需要一块很大的且连续的内存空间。如果数组元素能够无限添加,那么数组添加到一定的程度,可能就不存在这样一块连续的内存空间;而且数组的内存空间需要事先预留和分配,预留后才能保证数组的连续的内存空间不被其他数据占用。
三、数组是如何高效地随机访问?数组地址是如何计算的?首地址存放在哪?为什么必须是相同的元素?为什么索引从0开始?
数组的一个特点是可以根据下标随机访问数组元素,其时间复杂度为 O(1),那么它是如何实现的呢?
计算机分配的内存单元存储数据时,也会为内存单元分配一个地址,然后可以通过地址来访问内存中的数据。由数组的内存空间连续的特性,当需要访问某个元素时,它会通过下面的寻址公式来计算出该元素存储的内存地址:
其中 data_type_size 表示数组中每个元素的大小,base_address 就是数组的首地址。如在 int 型的数组 arr 中,data_type_size 就为 4 个字节。
那么数组的首地址是怎么知道的呢。其实数组名是一个指针,指向的就是数组的首地址。如上计算公式中的“arr”就是一个指针,指向数组的首地址,即base_address。
由于数组只保存一个首地址,其余地址都是通过计算公式得出,而在公式中的base_address(首地址) 和 data_type_size(元素大小)都是常量,变量只是 i(索引),由此得出由于元素大小固定,所以数组中必须是相同的元素才能保证元素大小固定。
当索引为0的时候,由公式可以看出 arr[0] 指向的是首地址,即数组中第一个元素,所以索引是从0开始的。
四、低效的“插入”和“删除”。
在数组中,为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效。
例如在插入操作中,假设数组的长度为 n,若我们要在数组的第 k 个位置插入一个数据,为了把第 k 个位置腾出来给新的数据,我们需要将第 k ~ n 这部分的元素都顺序地向后挪一位: arr[i] = arr[i-1] 。其时间复杂度为 O(n)。
而在删除操作中,若我们要删除数组的第 k 个元素,为了内存的连续性,就需要将第 k ~ n 这部分的元素都顺序地向前挪一位: arr[i] = arr[i+1] 。其时间复杂度为 O(n)。
“插入”和“删除”的优化
然而在很多我们不需要考虑数组中元素的有序性,数组只被当作一个存储数据的集合的时候,为了避免大规模的数据搬移,我们可以对插入和删除操作做一些优化。例如:
如果要将某个数据插入到第 k 个位置,可以直接将第 k 为的数据搬移到数组元素的末尾,然后将新的元素值直接赋值给第 k 个元素;
如果要将第 k 个元素删除,可以直接将数组的最后一个元素赋值给第 k 个元素,然后删除最后一个元素即可。
这样,其时间复杂度就会降为 O(1) 。
五、如何对数组进行扩容和拷贝
- 数组的扩容
数组是根据固定容量创建的,在必要的时候我们需要对数组 arr 进行扩容,方法则是初始化更大的数组,然后再将原数组的数据拷贝到新数组中。
在对数组进行拷贝时除了利用 for 循环遍历数组元素进行拷贝外,推荐使用更高效的 System.arraycopy() 方法。 - System.arraycopy() 方法拷贝数组
System.arraycopy() 使用 native 关键字修饰,大大加快程序性能,为 JVM 内部固有方法。它通过手工编写汇编或其他优化方法来进行 Java 数组拷贝,这种方式比起直接在 Java 上进行 for 循环或 clone 是更加高效的。数组越大体现地越明显。
该方法用于从指定源数组中进行拷贝操作,可以指定开始位置,拷贝指定长度的元素到指定目标数组中。 - 绝大部分数组和基于数组实现的容器(ArrayList ,栈等)的扩容都是基于 System.arraycopy() 方法进行操作的。
六、什么是偏移量,什么是索引。
偏移量是指数组空间起始位置的偏移值。这个定义有点难懂,换个简单的说法则是到数组起始位置的元素个数。
数组索引就是距数组首元素地址的偏移量。
如何计算偏移量
本文列举1-3维数组的计算方式。
- 一维数组:如arr[10],计算arr[4]的偏移量
如果下标从0开始,则 arr[4] 是第5个元素,相对于起始位置相差4个元素,所以偏移量为4。
如果下标从k开始,必要条件 k<=4 ,那么偏移量为4-k
。 - 二维数组:如以a[0…4][1…5]为例,计算a[2,2]的偏移量
如果以行为主序:偏移量d=in+j(i,j下标从0开始)。上述例子可知,j的下标是以1为起始,则此时的偏移量为:d=25+(2-1)=11
以列为主序:偏移量d=j*n+i
(i,j下标从0开始)。此时偏移量d=(2-1)*5+2=7
。 - 三维数组:如数组a[0…3,0…2,1…4],求a[2,2,2]的偏移量
三维数组计算a[i][j][k]的公式为d=i*n*o+j*o+k
则可求得d=2*3*4+2*4+(2-1)=33