数据结构与算法之美——数组
数组是一门编程语言中最基本的数据类型,也是一种最基础的数据结构。
数组是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。
线性表:
线性表就是数据拍成像一条线一样的结构,每个线性表上的数据最多只有前和后来个呢个方向。除了数组,链表、队列、栈等也是线性表结构。
连续的内存空间和相同类型的数据:
由于这两个限制,数组具有以下特点:
优点:“随机访问”;
缺点:低效:比如要想在数组中插入、删除,为了保证其连续性,就需要做大量的数据搬移工作。
数组支持数据的查找、插入和删除操作。在进行数组的插入、删除操作时,为了保证内存数据的连续性,需要大量的数据搬移,所以时间复杂度是O(n)。
很多人说,数组适合查找,查找的时间复杂度为O(1),这种说法是不准确的。数组确实适合查找操作,但是查找的时间复杂度并不是O(1),即便是排好序的数组,使用二分查找的时间复杂度也是O(logn),正确的表述应该是:数组支持随机访问,根据下标随机访问的时间复杂度是O(1)。
数组的插入操作:
假设数组的长度为n,现在我们需要在第k个位置插入一个数据,这样,我们需要将第k~n个元素顺序向后移一位。
时间复杂度分析:如果在数组的末尾插入元素,不需要移动数据了,这是的时间复杂度为O(1),但是在数组的开头插入元素,原本的所有的元素都需要依次向后移动一位,所以最坏时间复杂度为O(n),我们在每个位置插入元素的是等概率的,所以平均时间复杂度(1+2+3+...+n)/n = O(n)。
如果数组中的数据是无序的,数组只是被当作一个存储数据的集合,在这种情况下进行插入操作时,我们可以直接将原本的第k个数据元素放到数组的最后,把新元素放到第k个位置。这样,时间复杂度就会下降到O(1)。
数组的删除操作:
和插入操作类似,为了保证内存的连续性,需要搬移数据,平均时间复杂度也为O(n)。
警惕数组的访问越界问题:
示例代码:
//C语言
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!”,而是会无限打印“"Hello world!”,这是为什么呢?
原因就在于:数组大小为3,arr[0],arr[1],arr[2],而for循环中的约束条件为i <= 3,所以当i = 3时,数组arr[3]访问越界。在C语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。arr[3]就会被定位到一块不属于数组的内存上,而这个地址正好是存储变量i的内存地址,那么arr[3] = 0,就相当于i = 0,所以会导致代码无限循环。数组访问越界是C语言中的一种未决行为,没有规定这时编译器该如何处理。数组的访问本质就是访问一段连续的内存空间,只要数组通过偏移计算得到的内存地址是可用的,程序就不会报错。这时一般会出现会出现莫名其妙的逻辑错误,debug难度很大。很多计算机病毒也利用代码中的数组越界访问到非法地址的漏洞来攻击系统,所以,在编写代码时一定要警惕数组越界。