基本每一种编程语言中,都会有数组这种数据结构,虽然平时开发中用的很多,但并没有掌握它的精髓。
数组的特点
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
这句话中有三个关键字 “线性表”,“连续的内存空间”,“相同类型的数据”。
线性表
顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。
而与它相对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。
连续的内存空间和相同的数据结构
正是由于这两个特点,数组在 “随机访问”上效率很高,在 “插入” 和 “删除” 上效率比较低。
随机访问
计算机是通过地址来访问内存中的数据,而数组的内存地址计算公式如下:
a[i]_address = base_address + i * data_type_size
base_address 是数组内存块的首地址,data_type_size 表示数组中每个元素的大小,所以通过上述公式可以得知,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
插入和删除
数组为了保持连续的存储空间,所以在 “插入” 和 “删除” 方面比较低效。
因为每次插入一个数据时,这个数据后面的所有值都需要往后移动一位。删除一个数据时,这个数据后面的所有值都需要往前移动一位,时间复杂度都为O(n)。
在一些特殊场景下,可以对插入和删除操作做优化。
插入优化:
如果数组中的数据是无序的,如下图,只需把 c
移动到数组末尾,把 x
插入原先 c
的位置,其他数据就不需要移动了。
删除优化:
在某些特殊场景下,并不一定非得追求数组中数据的连续性。如果我们将多次删除操作集中在一起执行,就可以提高删除的效率。
这个其实也是 JVM 标记清除垃圾回收算法的核心思想。
容器和数组
针对数组类型,很多语言都提供了容器类,比如 Java 中的 ArrayList、C++ STL 中的 vector。
ArrayList 最大的优势就是可以将很多数组操作的细节封装起来。比如前面提到的数组插入、删除数据时需要搬移其他数据等。另外,它还有一个优势,就是支持动态扩容。
数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。
如果使用 ArrayList,我们就完全不需要关心底层的扩容逻辑,ArrayList 已经帮我们实现好了。每次存储空间不够的时候,它都会将空间自动扩容为 1.5 倍大小。
不过,这里需要注意一点,因为扩容操作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小。
参考:https://time.geekbang.org/column/article/40961