一、数组是什么?
数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。线性表结构包括数组、链表、队列、栈等数据结构。
非线性表,比如二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。
关于线性表和非线性表的其他数据结构类型我将会在后续的笔记中逐一介绍
二、如何随机访问数组元素?
正是因为数组是连续的内存空间和相同类型的数据,基于数组的这两个限制,我们可以对数组进行随机访问。
我们拿一个长度为10的int类型的数组a[10]来举例,计算机给数组 a[10],分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000。
当计算机需要随机访问数组中的某个元素时,它会首先通过下面的寻址公式,计算出该元素存储的内存地址:
a[i]_address = base_address + i * data_type_size
其中 data_type_size 表示数组中每个元素的大小。我们举的这个例子里,数组中存储的是 int 类型数据,所以 data_type_size 就为 4 个字节。
关于数组的一个面试题:
问:数组和链表的区别?
答:链表适合插入、删除,时间复杂度 O(1);数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
三、数组的插入和删除操作
数组为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效。
1.插入操作
假设数组的长度为 n,现在,如果我们需要将一个数据插入到数组中的第 k 个位置。为了把第 k 个位置腾出来,给新来的数据,我们需要将第 k~n 这部分的元素都顺序地往后挪一位。这就是数组的插入操作。
复杂度分析:
如果在数组的末尾插入元素,那就不需要移动数据了,所以,最好时间复杂度为 O(1)。但如果在数组的开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。 因为我们在每个位置插入元素的概率是一样的,所以平均情况时间复杂度为 (1+2+…n)/n=O(n)。
2.删除操作
如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。删除操作与插入操作类似,这里就不再做表述。
复杂度分析:
如果删除数组末尾的数据,则最好情况时间复杂度为 O(1);如果删除开头的数据,则最坏情况时间复杂度为 O(n);平均情况时间复杂度也为 O(n)。
四、数组访问越界问题
请看以下代码:
#include <stdio.h>
int main()
{
int a[10], i = 0;
for (; i <= 10; ++i)
{
a[i] = i;
printf("%d\n", a[i]);
}
}
当i = 10时,你会发现 a[10] 已经超过了 a 这个数组的长度范围,这时候就发生了数组越界问题。不同的编译器对数组越界的问题,处理的方式也不一样,在vc++6.0的编译器中,是按照内存地址递减的方式来给变量分配内存,在前一段代码中,地址分配如下:
栈是由高到低位增长的,所以,i 和数组的数据从高位地址到低位地址依次是:i, a[9], a[8]……a[1], a[0]。a[10]通过寻址公式,计算得到地址正好是 i 的存储地址,所以a[10] = 0,就相当于 i = 0。故,每次执行到 i = 10 的时候,i 就被赋值为0,程序永远出不来,成为了一个死循环。
而在一些比较高级的编译器中,当数组发生越界时会让程序停止运行,比如在vs2019中执行以上的代码就会出现如下情况:
前面说了,数组占用了一段连续的内存空间。然后,我们可以通过指定数组下标来访问这块内存里的不同位置。当你的下标超过数组的最大下标时,访问到的内存,就不再是这个数组所占用的内存。你访问的,将是其它变量的内存了。而这种情况往往会发生一些不可预知的后果,编译器为了防止这些严重后果的发生,开启了系统保护机制,将程序终止了。
所以,我们在对数组进行操作的时候一定要警惕数组访问越界的问题,否则可能就会发生一些不可预知的严重后果,对我们的电脑造成损伤。
在文章的末尾,我想提两个比较有意思的问题
1、为什么数组的下标都是从0开始的?
2、参照一维数组的寻址公式,你能给出二维数组的寻址公式吗?
下期文章的末尾给出答案……