一、什么是数组
1.1 定义
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
1.2 数组特性
-
支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
-
连续的内存空间、相同类型的数据:因此可以随机访问,但为了保证内存的连续性,需要做大量的数据搬移工作,使增删等操作变得非常低效。
-
线性存储,程序执行效率高。
1.3 时间复杂度
插入元素:O(n),所插入位置后面的元素都要向后移动。
删除元素:O(n),所删除位置后面的元素都要向前移动。
访问元素:O(1),使用索引访问。
查找元素:与使用查找算法有关,如顺序查找、二分查找、插值查找等。
1.4 空间复杂度
数组只用来存储指定类型的数据,所以存储n个数据的空间复杂度为O(n)。
二、基本操作
2.1 插入数据
数组插入数据,特定场景,特殊对待,如下:
-
数组需要保持有序:将插入位置
k
后面的数据元素依次后移,空出要插入的位置,再将新元素插入。O(n) -
数组不需要保持有序:将插入位置的旧元素移到最后,再在
k
处插入新元素。或直接将新元素放在最后。O(1)
2.2 删除数据
数组删除数据,特定场景,特殊对待,如下:
-
考虑内存连续性:
-
数组需要保持有序:删除
k
位置元素后,将后面的元素依次前移一个位置。O(n) -
数组不需要保持有序:用最后一个元素覆盖要删除的元素。O(1)
-
-
不考虑内存连续:
- 标记删除法,将要删除的元素标记(没有真正的删除),当数组空间不够时,一次性删除。但会造成内存不连续,如下图:
(一)存有abcdef
的数组,将bce
标记为删除(注意:此时数组的长度仍为6
),此时想在数组末尾存入g
(二)因空间不够,将没有删除的元素df
依次移动到a
后面a[1]、a[2]的位置,并将数组长度标记为3
(注意:此时a[3]、a[4]、a[5]的内存中仍存有def
,只是不再计入数组长度)
(三)将g
存入a[3]的位置。
这里澄清两个概念,数组空间
和数组长度
:
- 数组空间:数组所占内存空间,不随插入或删除元素而改变,除非申请空间
- 数组长度:数组所存储的数据元素个数,随插入或删除元素而改变
2.3 随机访问
以存储整型数据为例,
对于数组int a[]
,其地址为a
,第一个数组元素的地址和数组的地址相等,第二个数据元素的地址为a+4
(由于存储整型,所以加4),依次类推,每往后移动一个数据元素,地址加4,可见只要知道了数组的首地址,就知道了数组中每一个元素的地址,所以利用数组下标可以实现数组元素的随机访问。
随机访问寻址方法:a[i]_address = base_address + i * data_type_size
其中,base_address
为数组首地址,data_type_size
为每个元素的大小,依数据类型确定。
代码验证一下:
#include <stdio.h>
int main()
{
int a[] = {0, 1, 2, 3};
printf(" a.address: %p\n", a);
for (int i = 0; i < 4; i++)
{
printf("a[%d].address: %p\n", i, &a[i]);
}
}
打印结果为:
a.address: 0x7ffd46f51c50
a[0].address: 0x7ffd46f51c50
a[1].address: 0x7ffd46f51c54
a[2].address: 0x7ffd46f51c58
a[3].address: 0x7ffd46f51c5c
对于二维数组(多维数组),依然是线性存储的,寻址方式依然是通过元素下标。
二维数组寻址:对于 m * n 的二维数组, a [ i ] [ j ] ( i < m , j < n ) a [ i ][ j ] (i < m, j < n) a[i][j](i<m,j<n)的地址为:
a[i][j]_address = base_address + ( i * n + j) * data_type_size
2.4 查找数据
这里注意,查找和随机访问是不一样的,随机访问是给出索引(位置)返回数据元素的值,而查找一般是给出数据元素的值返回它的索引(位置)。
所以,数组查找操作的时间复杂度不是O(1),要看使用的查找算法,比如二分查找,时间复杂度为 O(logn)。
三、其他关于数组的问题
3.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;
}
这一段程序数组越界了,输出结果和内存地址分配方式有关。
函数体内的局部变量存在栈上,且是连续压栈。在Linux进程的内存布局中,栈区在高地址空间,从高向低增长。变量i
和arr
在相邻地址,在内存分配时,如果按照内存地址递减的方式进行分配,则i
比arr
的地址大,那么arr
越界正好访问到i
。当然,前提是i
和arr
元素同类型,否则那段代码仍是未决行为。这时无限循环输出hello world。
如果按照内存地址递增的方式进行分配,则i
比arr
的地址小,那么arr
越界正好访问不到i
,这时只输出4次hello world。
3.2 数组下标为什么从0开始?
从数组存储的内存模型上来看,下标
最准确的定义应该是偏移(offset)
。第一个元素的偏移为0,第i
个元素的偏移为i
。但如果从1开始,第i
个元素的偏移就为i-1
,多了一次减法运算,这将降低CPU的运算效率。(对于底层算法开发,效率优化要做到极致。)