一、前言
谈及数组,大家应该都很熟悉。数组是一种基础的线性表,在实际的编程过程及面试过程中数据的也是高频出现的数据结构之一,而且应用场合也涉及的非常广。而对于数组这种基本的数据结构,到底掌握了多少?
二、数组的特点
先简单回忆下数组:
String [] arr1 = new String [5];
int [] arr2 = {1,2,3};
从上面的代码中来分析下数组的基本特点:
1、可以存储基本数据类型,也可以存储引用数据类型;
2、数组的存储的是相同的数据类型;
3、数据是一种能线性表数据结构,是一组连续的内存空间,所以在除特定场景下,数组的插入和删除都需要进行数据搬移以保证空间的连续性。
如图:
三、强大的随机访问
数组的空间连续性特点也是使得数组可以实现随机访问,那么数组时如何实现空间访问呢?
众所周知,计算会给每一个计算机单元分配内存地址,计算机会通过地址来访问内存中的数据,计算机需要随机访问数组的元素的时候,会根据一个寻址计算公式,先计算出该元素所在的内存地址,在根据内存地址获取到数组的值。如下图:
图中的计算机分配了一个连续空间为10000~100015的连续空间,内存的默认首地址为base_adress=10000,寻址公式为
arr1[i]_address(下标数据)= base_address(首地址)+data_type_size (数组中元素的大小) * i。
时间复杂度分析:
所以根据下标随即访问数组时,时间复杂度是O(1),注意是根据数组下标,如果是获取是特定的值,那么实现的时间复杂度就不是O(1)了,举个例子:
假设一个存储了n个不同元素的String数组,想要得到是字符串"A”的元素,如果想要获取A,那么就需要遍历整个数组,如果A在数组的第一位,获取元素的动作依然是O(1),不过获取元素的过程的时间复杂呢?如果不是第一位,那么时间复杂度就是O(n),平时时间复杂度就是O(n)。不过如有利用二分查找算法的时候那么最坏时间复杂度就是O(logn)。所以在遇到面试过程遇到面试官提问数组的时间复杂度时不妨多想想几个场景。
数组的增删改三个操作在非特殊的场景下的时间复杂度都是f(n),而数组随即访问是f(1),随即访问的这样的杀手锏到底是如何实现现的呢?首先从数组的基本特点中连续的内存空间就是这杀手锏的切入点。
三、为什么插入和删除如此低效?
插入元素:如果从数组的非末尾新增一个元素的话,为保证数据空间的连续性,新增元素后面的元素会发生搬移的动作,此时的时间复杂度为O(n),而在末尾的新增元素不会发生数据搬移,此时的时间复杂度为O(1),所以平时情况时间复杂度O(n)。
还有一种情况:在无序数组中,可以把插入位置的前一个元素替换为新增元素,将替换的元素放置数组末尾,这样时间复度 仍然是O(1);
删除元素:同理,如果删除数组末尾数据,则最好的情况是O(1),如果删除数组中间元素为了保证内存的连续性,时间复杂度也是O(n)。不过当删除多个元素时,可以将元素要删除的运算先进行标记,等到要删除时在统一删除,这样一来就节省性能消耗。在JVM的GC算法中,标记清除算法就是这一原理。
四、总结下来:
数组的特点:线性表、连续的内存空间、相同的数据中类型、存储基本数据类型、存储引用数据类型。
优点:通过下标查找速度快,时间复杂为O(1),不过如有采取其在不知道数据所在位置的情况下,顺序遍历的情况也为O(n),而通过二分查找为O(logn)
缺点:增/删速度慢,数据搬移消耗性能,平均时间复杂度O(n)。
五、应用场景:
对于数组的应用场景,比如项目开发中用到的ArrayList的底层实现就是数组,而且增加了动态扩容(扩容原来数据的1.5倍),解决了数组不能自动扩展空间的问题,但是相比于数组ArrayList不能存储基本数据类型,在存储基本数据类型时会自动装箱为包装类,如果数据量较大的情况下,自动装箱和拆箱的性能消耗也不能不考虑。在业务开发中,容器一般是比较优先的选择,操作简单,性能也不会很受影响;如果是对性能比较敏感的框架开发等,则数组会成为优先选择。
如果大家喜欢可以关注我的公众号:大叔是个唐僧肉,近期会持续更新算法和数据结构的内容