3.数组
数组看起来简单基础,但是很多人没有理解这个数据结构的精髓。
1. 数组如何实现随机访问
1) 数组是一种线性数据结构(线性表),用连续的存储空间存储相同类型数据
I) 线性表:数组、链表、队列、栈
非线性表:树 图
II) 连续的内存空间、相同的数据(每个元素占有相同的内存空间),所以数组可以随机访问,但对数组进行删除插入,为了保证数组的连续性,就要做大量的数据搬移工作
a) 数组如何实现下标随机访问。
根据寻址公式,计算该元素的储存地址
a[i]_address = base_address + i * data_type_size #寻址公式
b) 纠正数组和链表的错误认识。数组的查找操作时间复杂度并不是O(1)。即便是排好的数组,用二分查找,时间复杂度也是O(logn)。
正确表述:数组支持随机访问,根据下标随机访问的时间复杂度为O(1)
2. 低效的插入和删除
1) 插入:最好O(1) 最坏O(n) 平均O(n)
2) 插入:数组若无序,并要求在K位置插入新的元素时,可以将第K个位置元素移动到数组末尾,把新的元素,插入到第k个位置,此处复杂度为O(1)。
3) 删除:最好O(1) 最坏O(n) 平均O(n)
4) 多次删除集中在一起,提高删除效率
记录下已经被删除的数据,每次的删除操作并不是搬移数据,只是记录数据已经被删除,当数组没有更多的存储空间时(数组操作可能边存储边删除),再触发一次真正的删除操作。即JVM标记清除垃圾回收算法。
3. 警惕数组的访问越界问题
4. 容器能否完全替代数组
相比于数字,java中的ArrayList封装了数组的很多操作,并支持动态扩容。一旦超过存储容量,扩容时比较耗内存,因为涉及到内存申请和数据搬移。
5. 为什么数组要从0开始编号,而不是从1开始
1) 从偏移角度理解a[0] ,0为偏移量,如果从1计数,会多出K-1。增加cpu负担。
为什么循环要写成for(int i = 0;i<3;i++) 而不是for(int i = 0 ;i<=2;i++)。第一个直接就可以算出3-0 = 3 有三个数据,而后者 2-0+1个数据,多出1个加法运算,很恼火。
2) 也有一定的历史原因
ArrayList是Array的复杂版本
ArrayList 最大的优势就是可以将很多数组操作的细节封装起来(提供简单的接口)。比如前面提到的数组插入、删除数据时需要搬移其他数据等。另外,它还有一个优势,就是支持动态扩容。
如果事先能确定需要存储的数据大小,最好在创建 ArrayList 的时候事先指定数据大小。
数组的用武之地:
1.ArrayList 无法存储基本类型,比如 int、long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,如果使用使用基本类型且特别关注性能,就可以选用数组。
2.如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
3.当要表示多维数组时,用数组往往会更加直观。比如 Object[][] array;而用容器的话则需要这样定义:ArrayList<ArrayList> array。
总结:对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。
浙公网安备 33010602011771号