1 数组是什么

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

1.1 线性表结构

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

  • 线性表(Linear List):顾名思义,线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。
  • 非线性表:是与线性表相对的概念,如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。

1.2 连续的内存空间和相同类型数据

  • 正是因为这两个限制,它才有了一个堪称“杀手锏”的特性:“随机访问”。
  • 注意:随机访问 != 查找O(1)
    最简单的二分查找复杂度也等于O(logn),正确的表述应该是数组支持随机访问,根据下标访问的复杂度为O(1)
  • 但有利就有弊,这两个限制也让数组的很多操作变得非常低效
  • 比如要想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作

1.3 为什么数组下标从0开始

  • 寻址公式:a[i]_address = base_address + i * data_type_size
  • “下标”最确切的定义应该是“偏移(offset)”,若用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址
  • C 语言中这么定义,别的语言都进行了效仿

2 低效的“插入”和“删除”

2.1 插入操作

  • 假设数组的长度为 n,现在,如果我们需要将一个数据插入到数组中的第 k 个位置。为了 把第 k 个位置腾出来,给新来的数据,我们需要将第 k~n 这部分的元素都顺序地往后挪一 位。
  • 如果在数组的末尾插入元素,那就不需要移动数据了,这时的时间复杂度为 O(1)
    但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)
    因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+ …n)/n=O(n)

2.2 删除操作

  • 跟插入数据类似,如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。
  • 和插入类似,如果删除数组末尾的数据,则最好情况时间复杂度为 O(1)
    如果删除开头的 数据,则最坏情况时间复杂度为 O(n)
    平均情况时间复杂度也为 O(n)

2.3 优化

  • 在某些特殊场景下,我们并不一定非得追求数组中数据的连续性
  • 比如,在插入是可以将原本k位置的元素移动到数组最后
  • 如果我们将多次删除操作集中在一起执行,插入.删除的效率会提高很多,且避免了多次拷贝
  • 比如,在删除时先记录下要删除元素,一遍遍历双指针就能完成删除

3 容器能否完全替代数组

3.1 容器优势(ArrayList)

  • ArrayList 最大的优势就是可以将很多数组操作的细节封装起来
  • 如前面提 到的数组插入、删除数据时需要搬移其他数据等
  • 支持动态扩容
  • 数组本身在定义的时候需要预先指定大小,因为需要分配连续的内存空间。如果我们申请了 大小为 10 的数组,当第 11 个数据需要存储到数组中时,我们就需要重新分配一块更大的 空间,将原来的数据复制过去,然后再将新的数据插入
  • 如果使用 ArrayList,我们就完全不需要关心底层的扩容逻辑,ArrayList 已经帮我们实现好 了。每次存储空间不够的时候,它都会将空间自动扩容为 1.5 倍大小
  • 这里需要注意一点,因为扩容操作涉及内存申请和数据搬移,是比较耗时的。所以, 如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小

3.2 数组优势

  • Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基 本类型,就可以选用数组
  • 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组
  • 当要表示多维数组时,用数组往往会更加直观。比如 Object[][] array[][];
    而用容器的话则需要这样定义:ArrayList array

总结

  • 对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。
  • 但如果你是做一些非常底层的开发,比如开发网络框架,性 能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

==> 复习总结

  1. 数组的插入,删除复杂度均为O(n), 但有一种优化手段,将删除的元素交换到最后,将插入位置元素放到最后
  2. 数组是随机访问,即按下标查找复杂度是O(1),普通查找复杂度也是O(n)
  3. 容器封装了数组的操作方法,如扩容等,使用更方便,适合开发使用
    但原生数组性能更高,更节约内存,适合底层开发,重新封装api