数据结构与算法-数组

一,前言

前面介绍了复杂度分析
这篇开始正式介绍数据结构和算法,首先介绍数组
不管你正在使用的是哪种编程语言,数组都是其最基础的数据结构

二,数组Array

数组是一种线性表数据结构,使用一组连续的内存空间来存储相同类型的数据

数组的特点:

1,线性表:
	数据排成一条线,数据只有前后两个方向
	
2,连续的内存空间,相同类型的数据
	由于这两个限制,数组天然支持"随机访问",但删除插入操作效率较低

数组的优势:

数组天然支持"随机访问",根据下标随机访问的时间复杂度为 O(1)

数组的劣势:

数组的删除和插入等操作效率较低
例如:向数组中插入或删除一个数据时,为了保证内存的连续性,需要做大量的数据搬移工作

三,数组的随机访问特性

数组可以根据下标随机访问数组中的元素

下图是长度为10的int型数组int[] a = new int[10]
数据结构与算法-数组_第1张图片
int型每个元素占4字节内存,
a[0]在内存中为1000~1003,
a[1]在内存中为1004~1007…
a[9]在内存中为1036~1039…

计算机为数组a[10]分配了一段连续的内存空间1000~1039
内存块首地址为base_address = 1000

当需要随机访问数组中某个元素时,通过寻址公式计算此元素的内存地址:

a[i]_address = base_address + i * data_type_size
data_type_size表示数组中每个元素的大小,如每个int类型占4

数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)

数组适合查找操作,但即便是排好序的数组,使用二分查找时间复杂度也是O(logn),
所以正确的表述应该是:数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)

四,低效的插入操作

数组为了保持内存数据的连续性,会导致插入操作比较低效

若数组长度为n,向第k个位置插入一个元素,需要将k~n元素整体向后挪动一位,以便把k的位置腾出来

最好情况时间复杂度:

如果在数组末尾插入元素,不需要移动数据,最好时间复杂度为O(1)

最坏情况时间复杂度:

如果在数组开头插入元素,所有数据都需要向后移动一位,最坏时间复杂度为O(n)

平均情况时间复杂度

长度为n的数组,在每个位置上插入元素的概率相同,均为1/n
加权平均为1*1/n + 2*1/n + 3*1/n + … + n*1/n = (1+2+3+…+n)/n
所以平均时间复杂度为(1+2+3+…+n)/n = O(n)

五,优化插入操作

如果数组中的元素必须是有序的,那么在第k个位置插入一个新元素后,就必须搬移k及k之后的数据

但如果数组中存储的数据没有任何规律,数组只用作存储数据的集合,
那么,可以将第k位数据直接搬移到数组的尾部,将新元素直接放入第k位置即可

例如,数组a[10]存储了5个元素:1,2,3,4,5

数据结构与算法-数组_第2张图片

在数组的第3个位置插入元素6,只需要将a[2]=3放入a[5],将a[2]赋值为6即可
插入完成后的数组a[10]=[1,2,6,4,5,3]

所以,在上述情况下,向数组第k位置插入元素的时间复杂度降为 O ( 1 ) O(1) O(1)


六,低效的删除操作

和插入类似,数组为了保持内存数据的连续性,会导致删除操作比较低效

若数组长度为n,删除数组中第k个位置的元素,为了内存的连续性,需要将k~n-1数据向前搬移一位

最好情况时间复杂度:

如果删除数组末尾元素,不需要搬移数据,最好时间复杂度为O(1)

最坏情况时间复杂度:

如果删除数组开头元素,需要搬移1~n-1个数组,最坏时间复杂度为O(n)

平均情况时间复杂度:

删除每个位置的元素概率相同,平均情况时间复杂度O(n)

七,优化删除操作

某些场景下,数组的删除操作并不需要追求数组中数据的连续性

可将多次删除操作集中在一起执行,就可以提高删除效率了

例如,数组a[10]中存储了8个元素:1,2,3,4,5,6,7,8,删除1,2,3三个元素

正常情况下,删除三个元素,数组中的其他元素都会做三次搬移操作
可以先记录下要删除的数据,删除操作只极力数据已被删除,
当数组没有更多空间存储数据时,再统一执行真正的删除操作
这样就大大减少了删除操作造成的数据搬移


八,容器

很多语言都提供了容器类,如java中的ArrayList

ArrayList的优势

将多种数组操作的细节封装起来,如:插入,删除时对数据的搬移

ArrayList支持动态扩容

当存储空间不足时,ArrayList会自动扩容至原来的1.5倍
但扩容操作会进行内存申请和数据搬移,影响效率
如果事先能够确定存储数据的大小,最好在创建ArrayList时指定数据大小

何时使用容器,何时使用数组

虽然ArrayList封装了Array,提供了诸多便利操作,但以下情况使用数组更为合适:

1,ArrayList无法存储基本类型,如int类型需要封装为Integer类型,
而java语言的自动装箱,拆箱会有额外的性能消耗
如果场景非常关注性能,或希望使用基本类型,可以选用数组存储

2,如果实现能知道数据的大小,并且对数据操作较为简单,用不到ArrayList封装的方法,可以使用数组

3,表示多维数组时,用数组更为直观,如:a[][]

使用容器,操作简单,但会损耗极少性能,使用数组可将性能优化到极致,适合底层或框架开发

九,为什么数组下标从0开始

前面介绍了寻址公式:

a[i]_address = base_address + i * data_type_size

从数组存储的内存模型看,下标表示的是"偏移"量
如果a表示数组的首地址,a[0]表示偏移量为0的位置,即首地址

如果数组从1开始计数,公式变为:

a[i]_address = base_address + (i-1) * data_type_size

这样每一次寻址都要多做一次i-1的减法运算,造成额外的消耗


十,总结

介绍了数组及其数据结构的特点
数组支持随机访问,但插入,删除操作效率较低
讨论了插入,删除操作效率较低的原因及特殊场景下的优化方案
介绍了容器类ArrayList
当存储空间不足时,ArrayList会自动扩容至原来的1.5倍
但ArrayList需要的内存空间也是连续的,有可能剩余量充足但没有连续的内存空间


维护记录:
20190419:更新数组随机访问图示,添加数组插入优化图示

你可能感兴趣的:(算法和数据结构)