1.什么是数组
数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相关类型的数据。
2.线性表与非线性表
线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。
除了数组,还有链表、队列、栈等也是线性表结构。
非线性表比如,二叉树、堆、图等。在非线性表中,数据之间并不是简单的前后关系。
3.数组的特性
随机访问。由于连续的内存空间和相同类型的数据这两个的限制,造就了这个特性。
4.数组的基本操作
4.1 随机访问
4.1.1 例子
以数组 int[] a = new int[10] 来举例。 计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。如下图:
计算机会给每个内存单元分配一个地址,通过地址可以访问到内存中的数据。当数组需要随机访问的时候,
首先会根据寻址公式确定地址。然后会根据地址去访问。
4.1.2 寻址公式:
一维数组的寻址公式:
a[i]_address = base_address + i * data_type_size
data_type_size 表示数组中元素的大小。比如int类型的数组为4个字节。
二维数组的寻址公式:
对于 m * n 的数组,a [ i ][ j ] (i < m,j < n)的地址为:
b[i][j]_address = base_address + ( i * n + j) * data_type_size
4.1.3 正确的描述
数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
注意:如果不是根据下标随机访问。直接说数组的时间复杂度为 O(1)是不正确的。因为即使为二分查找复杂度也是 O(logn),根本不是 O(1)。
4.2 插入操作
4.2.1 数组有序
设数组长度为n,插入到数组第k个位置。
如果是数组末插入,直接插入就好,时间复杂度为O(1)。
如果不是在数组末插入,为了腾出第k个位置,就需要把k到n这部分数据向后挪一位。
最坏的情况是数组首位插入,数组的所有数据都要向后移动一位。最坏时间度为O(n)
因为每个位置插入的概率是一样的,所以平均时间复杂度为(1+2+3+...+n)/n = O(n)
4.2.2 数组无序
如果数组是无序的情况下,除了上面的做法。为了减少数据大规模搬移。直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放到第K个位置。
假设数组 a[10] 中存储了如下 5 个元素:a,b,c,d,e。
将元素 x 插入到第 3 个位置c 放入到 a[5],将 a[2] 赋值为 x 即可。如下图:
这样的话,时间复杂度就变为O(1)了
4.3 删除操作
为了保持数组所占内存的连续性。还是需要搬移数据。
删除和插入类似如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。
某些特殊场景下,并不一定追求数组中数据的连续性。可以把多次删除操作合并成一次。
数组 a[10] 中存储了 8 个元素:a,b,c,d,e,f,g,h。需要依次删除 a,b,c 三个元素。
常规操作的话,是a,b,c分别删除3次,然后,d,e,f,g,h需要搬移3次。
换另外一种方式,a,b,c在删除的时候先做标记,而不是一次性删除。当数组没有足够空间的时候,一次性删除。这样的话就减少了数据的搬移次数。和JVM标记清除垃圾回收算法类似
5.数组访问越界的问题
Java 本身就会做越界检查。如果有数组越界的情况,就会抛出 java.lang.ArrayIndexOutOfBoundsException。
6.实际开发中数组和容器如何选择
6.1 如果是业务开发。直接用容器。如果已知需要存储数据的大小。在创建容器的时候就指定大小。
比如:
ArrayList<User> users = new ArrayList(10000);
for (int i = 0; i < 10000; ++i) {
users.add(xxx);
}
6.2 如果是基础框架的开发,能用数组还是要用数组。毕竟框架的话更需要考虑性能问题。
参考资料:《数据结构与算法之美》