数组

一、关键词

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

1.1 线性表

数据像一条线一样的排列,线性表上的数据只有前、后两个方向。线性表的杰出代表:数组、链表、队列、栈
在这里插入图片描述

1.2 非线性表

数据之间并非是简单的前、后关系,杰出代表:二叉树、堆、图等。
在这里插入图片描述

1.3 连续的内存空间和相同类型的数据

杀手锏:随机访问!

但是也有弊端,这两个特点使得数组的很多操作变得十分低效:想在数组删除、插入数据,为保证连续性,就需要做大量的数据迁移。

二、数组如何根据下标随机访问数组元素?

int[] a = new int[10]

比如说计算机给数组a[10]分配了一块连续内存空间1000~1039,内存的首地址为base_address=1000
在这里插入图片描述计算机会给每一个内存单元分配地址,通过这些地址来访问内存中的数据。通过以下寻址公式,计算出对应元素的存储的内存位置:

//内存首地址+i * 数组中每个元素的大小
a[i]_address = base_address + i * data_type_size

因为我们定义的数组中存储的是int类型的数据,所以data_type_size就是4个字节。

有一种不太准确的说法:“链表适合插入、删除,时间复杂度为O(1);数组适合查找,时间复杂度为O(1)”。数组的确适合查找,因为其内存空间连续的特点,但是时间复杂度并不能笼统概述为O(1)。如果用二分法查找的话,时间复杂度也是O(logn)。应该说数组支持随机访问,根据下标随机访问的时间复杂度为O(1)

三、插入、删除的低效性

数组由于内存空间连续的特点,会导致插入、删除操作的效率比较低下。原因几何?改进方式?

3.1 插入

int[] a = new int[n]

这个数组的长度为n,假如要把一个元素插入到k的位置,就需要把原来k~n这些位置的数据后移,给新来的腾地方。分析一下时间复杂度:插入的位置为数组的末尾,所有数据无需移动,时间复杂度为O(1);插入的位置为开头,所有的数据都需要后移一位,时间复杂度为O(n) 。每个位置插入元素的概率相同,故平均时间复杂度为(1+2+3+…+n)/ n= O(n)。这么处理的前提是,这个数组是有序的。

如果无序,这个数组就只能被看做是一个存储数据的集合,如果要在数组的K位置插入一个元素,为了避免数据的大规模迁移,最好得解决办法就是将原先位置K的数据迁移到数组最后,把新元素放到位置K上。此时,时间复杂度就是O(1)。这也是快排的处理思想。

3.2 删除

和插入类似,如果要删除第K个位置的数据,为保证内存连续性,必须得迁移“受影响数据”。如果要删除的元素位于末尾,时间复杂度为O(1)。如果位于开头,时间复杂度为O(n),平均复杂度为O(n)。

然而,有些时候并不要求数组中数据的连续性。换句话说,我们在意的并非是真正的删除,只要是“逻辑”上的删除就行。也就是说,当触发删除操作时,只需要对被删除数据暂时做好“标记“,等内存不够用了,再去执行真正的删除操作,将带有”删除“标记的数据全部删除掉,减少了数据迁移的成本。这利用了JVM的标记清除的垃圾回收算法的思想。

四、警惕数组问题

4.1 数组越界问题

数组最常见的就是越界,以下代码:

int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i<=3; i++){
arr[i] = 0;
printf("hello world\n");
}
return 0;
}

会无限循环的打印出“hello world”。原因在于当i==3的时候,数组就发生了越界。在C中内存空间是可以自由访问的(访问受限的内存空间除外),因此a[3]被定为在了某一块不属于数组的内存空间上,而这个内存空间就是存储遍历i的内存地址,所以就a[3]相当于i=0,于是代码就无限循环了。访问数组的本质,就是访问一段连续的内存空间,C并没有规定数组访问越界时编译器如何处理,只要数组能够通过偏移量的计算得到一块内存空间且可用,程序就不报错。

Java就不同了,因为Java本身会做越界检查。比如:

int[] a = new int[3];
a[3] = 10;

就会抛出java.lang.ArrayIndexOutOfBoundsException的异常

4.2 数组可替代性?

数组类型有很多的容器类,比如ArrayList。ArrayList最大的优势就在于它可以把很多数组操作的细节封装起来,如插入、删除数据时需要迁移其他数据等。它还可以支持动态扩容。

因为数组需要分配连续的内存空间,所以在定义的时候需要预先指定大小。假如申请了一个长度为10的数组,当第11个元素插入时,就需要重新分配一块更大的连续内存空间,将原有的10个数据copy过去后再插入第11个元素。

如果使用ArrayList,我们就完全不用考虑底层的扩容,因为ArrayList已经帮我们做好了。每次“空间不足”时,它自己就会自动扩容为1.5倍大小。

但是ArrayList的扩容操作非常耗时,因为涉及了内存空间的申请、数据迁移。因此,如果事先能够确定需要存储的数据大小,最好在new ArrayList时就指定大小,这样可以节省很多次的内存空间申请、数据迁移操作。

ArrayList users = new ArrayList(10000);
for (int i = 0; i < 10000; ++i) {
users.add(xxx);
}

五、总结

1.ArrayList无法存储基本类型,必须得封装为Integer、Long类。而Autoboxing、Unboxing又牵涉到了性能消耗。因此若重视性能,或偏向使用基本类型的可以选用数组。
2.如果数据大小提前知晓,且对数据的操作十分简单而用不到ArrayList提供的大部分方法,可以直接选用数组
3. 表示多维数组时,数组更为直观:Object[][] array;容器:ArrayList array
4.常规业务开发,直接使用容器就行。省时省力,损耗的那点性能完成能接受;如果是底层开发,就要好好斟酌一下了,数组优于容器。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值