数组是一种数据类型,本质是一种线性表数据结构,它用一组连续的内存空间,来存储具有相同类型的数据。
随机访问:
由于有了连续的内存空间和相同类型的数据,才能实现根据下标的“随机访问”。换句话说,相对于链表而言,数组适合查找操作,但是查找的时间复杂度并不为o(1),即便是排好序的数组,用二分查找时间复杂度也是o(logn),所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度是o(1)。
低效的“插入”和“删除”:
为了保持内存数据的连续性,导致数组在插入删除这来个操作的时候效率较低。
举个例子,假设数组的长度为n,如果我们要将一个数据插入到第K个位置,如果是在数组的末尾插入元素,则不需要移动元素,此时时间复杂度为o(1),如果是在数组的开头插入元素,那么所有的元素都要依次往后挪一位,此时为最坏的时间复杂度o(n)。
如果数组是有序的,那么在一个位置插入新的元素时,就需要按照以上方法,如果数组是无序的,这种情况下,为了避免大规模的数据搬移,有一个简单的方法,先将第k位的数据放到数组末尾。然后让新元素和第K个位置的元素互换,这个思想类似于快排。
删除操作也类似,一般情况下,删除元素以后需要搬移操作,但是为了避免连续删除操作导致的连续搬移操作,可以参考JVM的垃圾回收机制,先记录下已经删除的数据,每次的删除操作并不是真正地搬移数据,知识标记该数据被删除,当数据没有更多地空间存储数据的时候,再一次性全部删除。
容器类与数组:
以Java举例,其中的ArrayList讲数组的许多操作细节封装起来,如数组的插入、删除等等,还支持动态扩容。
数组本身在定义的时候需要预先指定大小用以分配连续的内存空间,如果存储空间不够用的时候,我们需要重新分配一个更大的空间,将原来的数据复制过去,再将新的数据插入。如果使用ArrayList,我们就不需要关心底层的阔i容逻辑,每次空间不够用的时候,它都会将空间扩容为1.5倍大小。但是由于扩容比较耗时,最好在创建ArrayList的时候事先指定数据的大小。
容器的使用并不意味着数组就无用武之地了:
1.Java ArrayList无法存储基本类型,如int、long,需要封装为 Integer、Long类,而Autoboxing、 Unboxing 则有一定的性能消耗。
2.如果数据大小事先一直,而且对数据的操作非常简单,用不到容器提供的大部分方法,也可以直接使用数组。
3.要表示多维数组的时候,用数组往往会更加直观,如Object[][]array,而用容器的话需要这样定义: ArrayList<ArrayList>array。
为什么数组要从0开始编号:
从数组存储的内存模型上来看,数组下标的确切定义应该是偏移(offset),如果用a来表示数组的首地址,那么a[0]就是偏移为0的位置:
a[k]_address = base_address +k*type_size
如果数组从1开始计数,那么每一次随机访问数组元素的时候就多了一次减法运算:
a[k]_address = base_address +(k-1)*type_size
写到这里,我再提一下二维数组的内存寻址:
对于m*n的数组,a[i][j](i<m,j<n)的地址为:
address = base_address +(i*n +j)*type_size