数组(Array)是一种线性表数据结构。他用一组连续的内存空间,来存储一组具有相同类型的数据。
线性表(Linear List)是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。除了数组(Array),链表(Linked List)、队列(Queue)、栈(Stack)也是线性表结构。
与之相对立的概念是非线性表,比如二叉树、堆、图等。之所以叫非线性表,是因为数据之间并不是简单地前后关系。
数组的特性:支持随机访问。但是元素的插入和删除低效。
1.数组根据下标位置随机访问数组元素。
编程中,我们初始化创建一个int类型的数组。例如:int[] test=new int[10];计算机会给数组test分配一块连续的内存空间例如是1000~1009,其中内存块的首地址是1000.计算机通过地址随机访问内存中的数据。计算机需要随机访问数组中的某个元素时,他会首先通过以下的寻址公式计算该元素的内存地址:
test[i]_address=test[0]_address+i*dataType_size
这里为什么数组下标从0开始,而不是从1 开始?因为从数组存储的模型来看,“下标(index)”最确切的定义是偏移(offset).test表示数组的偶首地址,test[0]就是偏移量为0的地址,即首地址,test[i]就是偏移i个dataType_size的位置。如果数组熊1开始计数,那计算test[i]的内存地址为:test[i]_address=test[1]_address+(i-1)*dataType_size。对比下,我们不难发现,从1开始编号,每次随机访问数组元素都多进行一个减法运算,对于cpu而言多一次指令,这显然是不划算的。还有就是历史原因,c语言的设计者用0开始计数数组下标,之后的高级语言也都纷纷效仿,减少不同语言学习的成本。
2.数组低效的"插入"和"删除"
插入操作:假设数组的长度为n,我们需要讲一个数据t插入到数组中的第k个位置。分析时间复杂度:如果实在数组末端插入元素,无需原有的任何元素,最好的时间复杂度为O(1);如果是在数组开头插入元素,则所有的元素都需要向后移一位,最坏的时间复杂度是O(n);在每个位置插入元素的概率一样,平均时间复杂度为(1+2+...+n)/n=O(n)
如果数组中的元素是有序的,则在某个位置插入新的元素时,必须按照上述的方法进行偏移,使其插入操作的数组依然为有序状态。但如果数组中原有的元素存储乱序,避免大规模的数据偏移。我们可以把带插入位置的原有元素放到数组最后面,新的元素放入指定下标位置。例如:tmp=test[i];test[i]=new_data;test[n+1]=tmp;
删除操作:和插入操作类似,如果删除末尾的元素,最好的时间复杂度为O(1);如果要删除开头的元素,最坏的时间复杂度为O(n);平均时间复杂度为O(n);
上述同样是在数组元素有序的情况下,但是在某些场景中,数组中的数据并非有序或者面临大规模的数据删除操作不追求数据的连续性,我们可以将多次的删除操作放在一起执行,提高删除操作的效率(具体:要执行的删除操作,我们首先记录下需要删除发的元素下标,最后执行真正的删除操作,这样大大减少数据偏移),这种方法类似jvm标记清楚垃圾回收算法。
小结:数组的优势在于支持元素随机访问。缺点在于低效的元素插入和删除,而且不支持动态扩展。
知识点补充(2019.01.29更新):
支持动态扩展的数组相关容器
针对数组类型,成熟的编程语言提供了容器类,例如Java的Arraylist,C++ STL中的vector。
Arraylist最大的优势就是将数组操作(数据的增删查改)的细节已经封装起来,而且他还有一个优势就是支持动态扩容。
数组自身在初始化的时候需要预先指定大小,因为需要分配连续的内存空间,例如int[] array=new int[10];如果第11个数据需要存储在数组中,我们需要重新分配一块各大的空间,将原来的数据复制过来,然后再将新的数据插入。
如果使用Arraylist,我们无需关注底层的扩容逻辑,Arraylist已经帮我们实现好,每次存储存储空间不够使,他会自动将空间扩容为原来1.5倍大小(int newCapacity = oldCapacity + (oldCapacity >> 1);)。
下图为相关部分的详细源码:
/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
这里需要注意的是,因为扩容操作涉及内存申请和数据搬移,这是比较耗时的。因此,如果我们能够实现确定需要存储数据的大小,最好在创建Arraylist的时候事先指定数据大小。例如,我们需要从数据库中取出1000条数据放入Arraylist中。
ArrayList<User> users = new ArrayList(10000);
for (int i = 0; i < 10000; ++i) {
users.add(xxx);
}
最后,我对数组array和基于数组的容器Arraylist做个基本的总结:
1.Java中的Arraylist存储的是包装类Integer Long类 不是int long这些基本的数据类型,而我们常说的拆箱 封箱操作也需要一定的性能消耗。
2.如果数组的大小实现已经知道,而且不需要考虑动态扩容,建议直接使用数组。
3.在表示多维数组时,利用数组会更加直观。例如int[][] a=new int [m][n];但是 利用容器的话,Java中是Arraylist<Arraylist> array
基础面试常考: 简要说出数组和链表的区别,以及在数据增删查改上的性能分析?
数组Array:顺序存储,它利用一段连续的内存空间去存储相同数据类型的数据。数组的特性是支持数据的随机访问(根据数据下标实现O(1)的数据查找,但是其数据的删除 与插入低效),而且数组不支持动态扩展,数组在初始化的时候就定义好了容量size
链表(Linkedlist):链式存储,他能利用物理上零散的内存空间存储逻辑上连续的相同类型的数据。他通过指针实现将零散的内存串联起来使用。链表的特点是支持高效的插入 删除操作,但是不支持元素的随机访问。
在链表中随机访问某个元素,无法想数组那样通过首地址和下标(通过寻址公式直接计算出对应的内存地址)高效的查找,链表需要根据指针从头结点出发一个节点一个节点的一次遍历,所以在链表中查找等于某个特定值的节点的时间复杂度是O(n) ;针对链表的插入和删除操作,只需要考虑相邻节点的指针改变,其对应的时间复杂度为O(1)