数据结构与算法(二):数组

  数组是数据结构中最简单,基础的结构,并且也是我们刚接触代码就会学习到的数据结构。那么我们对数组真的足够了解了吗?

定义

  数组是一种线性表数据结构,它用一段连续内存空间,来存储一组具有相同类型的数据。

  对于上面数组的定义有几个关键点:线性表,连续内存空间,相同类型数据。

  线性表,顾名思义,就是数据像一条线一样串联的数据结构,结构中的每条数据最多只有前后两个方向,链表/栈/队列都属于线性表数据结构。而树/图等数据结构因为数据不是简单的前后关系,所以不属于线性表结构。

              

   连续内存空间和相同类型数据则是数组最重要的特征,它帮助我们提升了根据数组下标随机访问数据的速度。可以直接通过寻址公式(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;
}

 

posted @ 2021-04-09 10:30  唐磊(Jason)  阅读(55)  评论(0编辑  收藏  举报