数组
- 一种线性表数据结构
- 一组连续的内存空间
- 存储一组具有相同类型的数据
线性表(Linear List)
数据排成一条线一样的结构
- 数据最多只有前和后两个方向
tips:除了数组,链表、队列、栈等也是线性表结构
非线性表
数据之间并不是简单的前后关系
tips:比如二叉树、堆、图等
连续的内存空间和相同类型的数据
正因如此,才有了“随机访问”的特性
数组如何实现根据下标随机访问数组元素?
通过如下寻址公式,计算出该元素存储的内存地址
a[i]_address = base_address + i * data_type_size
纠正一个常见的错误:
问:数组和链表的区别
答:数组适合查找,查找时间复杂度为O(1) ,链表适合插入删除,时间复杂度为O(1)
补充:数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。即便是排好序的数组,用二分查找,时间复杂度也是O(logn)
低效的“插入”和“删除”
插入操作
问:假设数组长度为n,将数据插入到数组的第k个位置
答:
-
有序数组:需要把k~n这部分元素顺序往后挪一位
如果在数组末尾插入元素,那么就不需要移动数据,时间复杂度为O(1)
如果在数组开头插入元素,那么所有数据往后移动一位,最坏时间复杂度是O(n)
因为在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为(1+2+…n)/n=O(n) -
无序数组:将第k位的数据搬移到数组元素的最后,把新的数据直接放入到第k个位置
此时时间复杂度就会降为O(1)。
此思想在快排中也会用到
删除操作
和插入类似,如果要删除第k个位置的数据,为了内存连续性,也需要搬移数据
删除末尾时间复杂度O(1)
删除开头时间复杂度O(n)
平均情况时间复杂度O(n)
实际情况中
- 数组空间够用时:将要删除的元素进行标记,进行逻辑删除
- 数组空间不够时:统一进行删除,减少数据搬移次数
JVM标记清除垃圾回收算法的核心思想就是如此,mysql的b+树上删除也是这么做的
数组的访问越界问题
由于访问数组的本质就是访问一段连续内存,只要数组通过偏移计算得到的内存地址是可用的,那么程序就可能不会报任何错误。
java:会抛出ArrayOutOfIndexException
c:需要自己检查处理
容器能否完全替代数组
数组:定义时需要预先指定大小,因为需要分配连续的内存空间
如果已经申请了大小为10的数组,当需要存储第11个数据时,需要重新分配一块更大的空间,将原先的数据复制过去,然后再将新的数据插入
ArrayList:无需关心底层扩容逻辑,每次存储空间不够,将空间自动扩容为1.5倍大小(空间不足则新增一半)
由于扩容操作涉及内存申请和数据搬移,比较耗时,所以如果事情能确定数据大小,最好在创建时事先指定数据大小
数组更合适的场景
- ArrayList无法存储基本类型,比如int、long,需要封装为Integer、Long类,如果特别关注性能,或者希望使用基本类型,就可以使用数组
- 事先大小已知可以考虑
- 多维数组
- 非常底层的开发,比如网络框架
解答开篇
如果从0开始,计算a[k]的内存地址公式为
a[k]_address = base_address + k * type_size
如果从1开始,计算a[k]的内存地址公式为
a[k]_address = base_address + (k-1)*type_size
从1开始,每次随机访问数组元素都多了一次减法运算!