标签:性能 元素 历史 内存 编译器 线性 不同 dex 情况
数组是一种线性表数据结构,它用一组连续的的内存空间,来存储一组具有相同类型的数据。
基于数组的两大特性:线性表结构和连续的内存空间和相同类型的数据,使得数组可以通过下标直接访问数组元素,从而具有“随机访问”的特性。
具体实现方法:
当计算机需要随机访问数组中的某个元素时,通过寻址公式计算出该元素存储的内存地址,寻址公式如下所示:
a[i]_address = base_address + i * data_type_size
其中 i 为数组元素的下标(从0开始),data_type_size 表示数组中每个元素的大小,假如数组中存储的是 int 类型数据,那么 data_type_size 就为4个字节。
根据下标随机访问的时间复杂度为 O(1)。
注意:前提条件是根据下标进行的随机访问,如果是在下标不确定的情况下查找某个元素,那么即便是已经排好序的数组,你用二分查找,时间复杂度也是 O(logn)。
数组为了保持内存数据的连续性,会导致插入、删除操作比较低效,具体是为什么呢?
每当在数组中插入一个新元素的时候,为了给这个新插入的元素挪出一个位置,我们可能需要移动大量的元素。
假设数组的长度为 n,如果在数组的末尾插入元素,那就不需要移动数组了,所以最好时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的元素都要一次往右移动一位,所以最坏时间复杂度为 O(n)。
因为我们在每个位置插入元素的概率是一样的,所以平均时间复杂度为 (1+2+3+……+n) / n = O(n)。
和插入数据类似,如果我们要删除某个位置的数据,为了内存的连续性,也需要搬移数据。
类比插入数据,如果删除数组末尾的数据,那么就不需要移动数组了,所以最好时间复杂度为 O(1);如果删除开头的元素,则整个数组都要移动,所以最坏时间复杂度为 O(n);同理可知,平均时间复杂度为 O(n)。
数组越界在 C 语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。因此,在 C 语言中出现数组访问越界的情况时,会出现莫名其妙的逻辑错误,例如,访问了非法地址、陷入死循环等等。
不同于 C 语言,Java 本身就会做数组越界检查,比如下面几行代码,就会抛出 java.lang.ArrayIndexOutOfBoundsException。
1 int[] a = new int[3]; 2 a[3] = 10;
针对数组类型,很多语言都提供了容器类。比如 Java 中的 ArrayList、C++ STL 中的 vector。
以 Java 的 ArrayList 为例,ArrayList 最大的优势就是可以将很多数组操作的细节封装起来,比如前面所说的数组插入、删除数据时需要移动其他数据等;另外,它还支持动态扩容。
我们在定义数组的时候通常需要预先指定大小,以便计算机分配连续的内存空间,当预先指定的空间不够用时,则需要重新分配一块更大的空间并将原来的数据复制过去,然后再继续插入新的数据。如果使用 ArrayList,每次内存空间不够用的时候,它都会讲空间自动扩容为 1.5 倍大小,这样我们就完全不需要关心底层的扩容逻辑了。
1. 以 Java 的 ArrayList 为例,ArrayList 无法存储基本数据类型,比如 int、long,需要封装为 Integer、Long 类。这种时候,选用数组可减少一定的性能消耗。
2. 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
对于业务开发,直接使用容器就足够了,只需要牺牲一点点的性能,就能省下大量时间。但如果是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候就应该使用数组。
前面我们讲过寻址公式:
a[i]_address = base_address + i * data_type_size
所谓下标,其实是一个“偏移量”,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:
a[i]_address = base_address + (i-1) * data_type_size
对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令,在一定程度上降低了一点效率。
还有一个历史原因:C 语言设计者用 0 开始计数数组下标,之后的许多高级语言都沿用了这一习惯。
标签:性能 元素 历史 内存 编译器 线性 不同 dex 情况
原文地址:https://www.cnblogs.com/hardyyao/p/9744606.html