《数据结构和算法之美》专题陆陆续续看了好几篇了,看到数组这篇了。刚看的时候心想着数组这东西基本都会用,还能讲出花来?看完后发现我还是有蛮大收获的,下面大概总结下。
数组是再寻常不过的数据结构了,几乎所有的编程语言应该都有数组这种结构吧,比如C、Java、Python。
什么是数组
数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
两个要点
线性表
好久没有听过这个名词了,但是多少还有些印象。线性即表示是一条线,只有前和后,区别于非线性。
线性数据结构包括数组、链表、队列和栈等。
非线性,显然就不是一条线,其表现形式有多种,比如我们数据结构中学到的树、堆和图等。
连续内存空间和相同类型数据
可以简单的将随机访问的特性和连续内存空间和相同类型数据等价。上面的两个条件的限制,好比一个萝卜一个坑,从而使数组可以支持随机访问。
没有什么东西是完美的,数组也不例外。有了随机访问的优点,自然也有自己的不足,那就是插入和删除比较麻烦,时间复杂度较高。
数组插入和删除的时间复杂度
文中提到在面试的时候,经常会问到数组和链表的区别。很多人会提到一点:链表适合插入和删除,时间复杂度为O(1),数组适合查找,时间复杂度为O(1)。
这个解答不够严谨和准确。数组的查找时间复杂度不是O(1)。即使是排好序的数组,使用二分查找,时间复杂度也是O(logn)。
所以严谨表述应该是,数据支持随机访问,根据小标访问的时间复杂度为O(1)。
同时,链表插入的时间复杂度也不能单纯认为是O(1),因为首先需要定位到要插入或删除元素的节点位置,这个时间复杂度为O(n)。所以,严谨的说,是找到了要插入或删除的元素再进行操作的时间复杂度为O(1)。
重新看数组的插入和删除
数组插入
对于数据的插入,我的第一反应是找到要插入的位置,然后将所有后面的位置平移一个位置,这样的操作的时间复杂度是O(n)。
但是实际上,我们可以不用这么墨守成规,或者我们换个思路,把要插入的位置比如是k,该位置对应的元素移到数组的最后一位,然后将要插入的元素插入原来的k位置。
对于可能引起的数据无序可以通过其他高效算法接口,可以将时间复杂度降为O(logn)甚至O(1)。
所以算法是个很有趣的事情,一点思想的转变,可能就会带来性能的提升。
数组删除
数组删一般来说也是找到要删除的元素,将后面的数据都平移一个位置,时间复杂度为O(n)。
对于需要删除的多个元素,我们可以不用那么实诚,每次都切切实实的平移受影响的元素。我们可以标记需要删除的元素,最后一次性调整所有需要移动的元素。该思想和前段时间看JVM垃圾回收中的标记清楚的算法如出一辙,秒!
其他注意事项
数据越界是常见的问题,要警惕
容器和数组各有所长,写底层框架还是用数组好,顺带提醒,使用容器时如果能确认容器大小,最好声明下,因为扩容设计的内存申请和数据搬迁成本较高
ArrayList等无法存储基本类型,多维数组的标识要比容器标识更易于理解
为什么大多数编程语言,数组从0开始编号,而不是1开始
鉴于前面说的数组是采用连续的内存地址存放相同类型的数据。
所以,从内存层面我们访问数据其实是在平移到不同的内存地址,然后获取该内存地址对应的数组元素值。
那么,数组是如何获取指定位置的元素值的?
我们通过偏移量和首地址访问需要访问的元素。如果数组从0开始,那么a[0]就是偏移量为0的位置,a[k]就是偏移量为k的位置。所以计算a[k]的内存地址的公式如下
a[k]_address = base_address + k*type_size
如果数组从1开始,那么公式就变为
a[k]_address = base_address + (k - 1)*type_size
这样,每次随机访问元素时就会多一次减法指令运算,显然,应该减少或避免这样的可优化的指令,所以数组从0开始。