算法与数据结构之美-数组

算法与数据结构之美-数组

  • 想说的话
  • 开篇思考
    • 如何实现随机访问
    • 低效的插入和删除
    • 容器能否完全替代数组?
    • 解答开篇
    • 参考

想说的话

写博客的意义,是为了分享自己学到的知识与大家共同进步。下面的内容,都是我在极客时间上学习的一门 数据结构与算法之美——王争老师的课,如果感兴趣的话,大家可以去购买,我也不是完全照搬内容,不是为了发博客而发,也是相当于自己的学习笔记,留言区可以留下问题,我们共同探讨!

开篇思考

为什么许多编程语言中数组都是从0开始编号?

如何实现随机访问

数组(Array)是一种线性表数据结构,用一组连续的内存空间,来存储一组相同类型的数据。线性表(LinearList):数组,链表,队列,栈等数据结构
算法与数据结构之美-数组_第1张图片
由于数组是需要连续的内存空间和相同类型的数据,才能够支持“随机访问”,带来的弊端就是为了保证数据的连续性,使得数据的增删需要进行大量的数据搬移。

创建一个长度为10的int类型的数组int[ ] a = new int[10],计算机给a[10]分配了一块连续空间1000-1039,内存块的首地址为base_address = 1000,图如下所示:
算法与数据结构之美-数组_第2张图片
计算机会给每个内存单元分配一个地址,计算机通过地址来访问内存中的数据,当计算机通过地址访问数据时,需要通过寻址公式来计算出元素存储的内存地址:
a[i]_address = base_address + i * data_type_size;其中data_type_size代表了数组中每个元素的大小,上图中的int类型的数据,data_type_size就为4个字节;

二维数组内存寻址:
对于 m * n 的数组,a [ i ][ j ] (i < m,j < n)的地址为:
address = base_address + ( i * n + j) * type_size

数组只有按照下标进行随机访问的时候时间复杂度为O(1);

低效的插入和删除

先看看插入操作:
如果在数组的末尾插入新的数据,时间复杂度恰好为O(1),如果在数组的开头插入一个元素,时间复杂度就是O(n).因为每个位置的插入概率是相同的,所以平均时间复杂度是(1+2+…n)/n=O(n)。

对于删除操作:
如果删除第K个位置的数据,为了保证内存的连续性,也需要搬移数据,保证内存的连续性,其平均时间复杂度也是O(1)。实际上,在某些特定的场景下,并不需要追求数组的连续性,每次删除操作就是对要删除的数据做标定,可以将多次删除操作集中在一起执行,类似于JVM中的标记-清除算法,这样可以提高删除的效率。

容器能否完全替代数组?

Java中对于数组类型提供了容器类-ArrayList

ArrayList 基于动态数组实现,RandomAccess接口标识着其支持快速随机访问;

public  class ArrayList<E> extends AbstractList<E>
	implements List<E> , RandomAccess,Cloneable,java.io.Serializable
//数组的默认大小为10
private static final int DEFAULT_CAPACITY = 10;

ArrayList的优势是将许多对于数组的操作封装起来,例如:数组插入、删除时数据搬移等,还有一个优点是支持动态扩容。

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

public boolean add(E e){
	ensureCapacityInternal(size+1); //increments modcount!!
	elementData[size++] = e;
	return true;
}
private void ensureCapacityInternal(int minCapacity){
	if(elementData == DEFAULT_EMPTY_ELEMENTDATA){
		minCapacity = Math.max(DEFAULT_CAPACITY,minCapacity);
	}
	ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity){
	modCount++;
	if(minCapacity - elementData.length>0)
		grow(minCapacity);
}

private void grow(int minCapacity){
	int oldCapacity = elementData.length;
	int newCapacity = oldCapacity+(oldCapacity >>1 );
	if (newCapacity - minCapacity < 0)
		newCapacity = minCapacity;
	if (newCapacity - MAX_ARRAY_SIZE > 0)
		newCapacity =  hugeCapacity(minCapaciy);
	elementData = Arrays.copyOf(elementData,new Capacity);	
}

至于Array和ArrayList的选择,根据实际需要:
对于业务开发,直接使用容器,省时省力,即便损耗一丢丢性能,完全不影响整体性能;但是对于底层框架的开发,性能需要做到极致,数组就会优于容器。

解答开篇

从数组的内存模型来看,“下标”最为确切的定义是“偏移(offset)”,如果a来表示数组的首地址,a[0]就是偏移量为0的位置,a[k]就表示偏移k个type_size的位置,所以计算a[k]内存地址只需要使用上面的寻址公式即可。

参考

[1]: 极客时间-数据结构与算法之美-数组

你可能感兴趣的:(数据结构,数组,数据结构与算法之美)