数据结构与算法(二):数组
数组是数据结构中最简单,基础的结构,并且也是我们刚接触代码就会学习到的数据结构。那么我们对数组真的足够了解了吗?
定义
数组是一种线性表数据结构,它用一段连续内存空间,来存储一组具有相同类型的数据。
对于上面数组的定义有几个关键点:线性表,连续内存空间,相同类型数据。
线性表,顾名思义,就是数据像一条线一样串联的数据结构,结构中的每条数据最多只有前后两个方向,链表/栈/队列都属于线性表数据结构。而树/图等数据结构因为数据不是简单的前后关系,所以不属于线性表结构。
连续内存空间和相同类型数据则是数组最重要的特征,它帮助我们提升了根据数组下标随机访问数据的速度。可以直接通过寻址公式(arry[i]_address=baseAddress+i*typeSize)访问数组下标所在位置的数据。但有利也有弊,这个特征让数组的插入和删除操作变得非常低效,比如想在数组中插入或者删除一个数据,为了保证数据连续性,就需要做大量的数据搬移工作。
时间复杂度
随机插入数据的时间复杂度:假设要在n长度的数组中第k位插入一条数据,需要将数据插入到第k位置,并将第k位原来的数据向后迁移,这样第k-n位的数据都要向后移动一位。当在数组的末尾插入一条数据时,因为不需要数据移动,所以它的时间复杂度是O(1),而在数组的开头插入数据时,则是所有的数据都需要向后挪动一位,所以它的时间复杂度是O(n),因为我们在每个位置插入数据的概率是一样的,所以平均情况的时间复杂度是(1+2+3+...+n)/n=O(n)。
删除数据与插入数据的操作类似,所以它们的时间复杂度相同,但是在一些特殊场景下,我们并不一定非要追求数组中数据的连续性,当要删除数据时,我们仅将要删除的数据标记为已删除,但不挪动位置,当数组内存空间不足时,我们再触发一次删除操作(将标记已删除数据删除),这样就大大减少了删除操作导致的数据搬移(JVM 标记清除垃圾回收算法的核心思想)。
查询数据的时间复杂度:分为两种情况,第一种是根据数组下标查询数据,因为我们根据寻址公式可以计算出数组下标数据的内存地址,所以时间复杂度为O(1)。第二种情况是根据数据内容查询,这种情况下,如果数组是有序的,通过二分查找时间复杂度是O(logn),如果数组是无序的,只能通过遍历,那么查询的时间复杂度就是O(n)。
访问越界
在C语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的,如果数组下标越界,就会访问到不属于数组的内存空间上的数据,这样可能会导致严重的系统错误。而在高级语言中,当数组下标越界时,也会在运行时抛出数组下标访问越界的异常,可能使程序进入不可用的状态。所以我们需要格外注意可能造成数组下标访问越界的场景,例如for循环。
变长数组
我们知道,数组的长度在声明后是无法修改的,但可以通过复制的方式实现数组的扩容,从而实现一个变长数组。
// 全局变量,大小为10的数组array,长度len,下标i。 int array[] = new int[10]; int len = 10; int i = 0; // 往数组中添加一个元素 void add(int element) { if (i >= len) { // 数组空间不够了 // 重新申请一个2倍大小的数组空间 int new_array[] = new int[len*2]; // 把原来array数组中的数据依次copy到new_array for (int j = 0; j < len; ++j) { new_array[j] = array[j]; } // new_array复制给array,array现在大小就是2倍len了 array = new_array; len = 2 * len; } // 将element放到下标为i的位置,下标i加一 array[i] = element; ++i; }