国庆假期就这么过去了,假期没带电脑回家,写文章不太方便,于是就干脆没写。以至于积攒了太多要写的东西,只能慢慢补上了,开写。
提到「数组」,可以说是再熟悉不过了。下面是 维基百科 对数组(array)的定义:An array is a systematic arrangement of similar objects, usually in rows and columns. 意思就是说,数组存储了一些具有相同数据类型的元素。说得更具体一些就是:数组是一种线性表结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
数组的特性
首先看第一个特性- 线性表。数组、链表、队列、栈都是线性表结构,数据之间只有前后两种方向。二叉树、堆、图是非线性表结构,数据之间不仅仅有前后两种方向。
再看剩余的两个特性-连续的存储空间 和 相同类型的数据。正是这两个特性,决定了数组具有根据下标随机访问的功能,根据下标访问的时间复杂度为 O(1)。但是也是由于这两个特性,导致 插入和删除 操作的时间复杂度比较高。
例如在数组arr = [ a, b, c, d ] 中的元素 a 前插入一个元素 e,需要将 a、b、c、d 元素依次后移一个存储单元,然后才能完成插入操作。
例如在数组arr = [ a, b, c, d ] 中,将元素 b 删除,在删除 b 后,为了保证存储的连续性,需要将 c,d 往前挪一个存储单元,才能完成删除操作。
有一个面试题是这样的,说一下链表和数组的不同点。可以这么回答:链表 适合删除和插入操作,时间复杂度为 O(1),数组 适合根据下标进行查找操作,时间复杂度为 O(1)。一定要注意 根据下标 这四个字,否则时间复杂度就不一定是 O(1) 了。
数组的访问越界
数组的越界问题需要非常重视,特别是在 C 语言中。先看一段 C 语言代码:
int main () {
int i = 0;
int arr[3];
for (; i <= 3; i++) {
arr[i] = 0;
printf("Hello World!");
}
return 0;
}
运行这段代码,会不停地打印 "Hello World!"。这是因为,当 i 等于 3 时,arr[3] 的地址存储的是变量 i ,当 arr[3] = 0时,即 i = 0,所以程序会不停地打印 "Hello World!",这就是数组访问越界的结果。
数组的扩容
一个数组申请了能存储 10 个整型变量的内存空间,当要存储 11 个整型变量时,就需要数组扩容。例如在 Java 中,会用 ArrayList 进行动态扩容,即,当要存储的变量个数超过之前申请的变量个数时,会将数组大小扩充为原来的 1.5 倍 ,需要将原来的数组拷贝到这 1.5 倍的内存空间里,是比较耗时的。一般最好先申请好需要的内存空间,避免数据的拷贝。
数组下标从 0 开始
刚开始学 C 语言的时候,我就纳闷,为啥数组的下标要从 0 开始,就是为了与众不同吗,当时也没再往深处想,现在大体知道了里边的原因。
一种说法是,数组可以根据下标进行元素的查找,这是因为存储空间连续、元素数据类型相同。对每个元素的访问是这样的:
Find_address = Base_address + k* Type_size
- 要查找元素的地址 Find_address
- 基地址 Base_address
- 偏移量 k
- 数据类型大小 Type_size
这里的下标就代表偏移量 k ,当下标从 1 开始时,这里的 k 就变成了 k-1 ,所以 CPU 要多进行一次减法操作,所以下标从 0 开始更省计算力。
另一种说法是,C 语言最先规定了数组下标要从 0 开始,之后的 Java 等语言效仿也让数组下标要从 0 开始。但是 Matlab 的下标从 1 开始, Python 的下标还可以是负数。
小结
要明确数组的定义,知道数组的优缺点,特别要注意数组访问越界问题,数组元素的插入和删除操作尽量要用一些算法降低时间复杂度。