飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • UEditor使用文档
  • AngularJS教程
  • ThinkPHP5.0教程

数据结构 - 数组

时间:2022-02-11  作者:fatedeity  

简介

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

线性表

数组结构

如上图所示,线性表就是数据排成一条线一样的结构,因此每个线性表中的数据只有前后两个方向。像数组、链表、栈、队列都是线性表结构。

与线性表对应的就是非线性表结构,非线性表中的数据会存在多个方向,数据之间不是简单的前后关系。像树、堆都是非线性表结构。

连续的内存空间

数组存储使用的内存空间是连续的,在数组存储的这一片空间只会存储数组元素,不会再拆分出来存储其他的数据。

相同类型的数据

在数组的原始定义中,数组只能存储相同类型的数据,这样可以保证数组中每个元素占用的内存空间都能保持一致。

正是数组存在“连续的内存空间”和“相同类型的数据”这两个限制,才让它拥有了随机存取这个高效的特性,但同时也使得数组在插入、删除数据的时候会比较低效。

数组的特性

就数组增删改查的操作而言,数组拥有高效随机存取的特性,也存在低效插入、删除的劣势。

随机存取

这里的“随机存取”指的的是通过下标访问数组元素,然后对这个元素做存取操作。

计算机会给每个内存单元分配一个地址,然后通过地址来访问内存中的数据。当计算机需要随机访问数组中的数据时,它可以通过下面的寻址公式来计算出对应下标的内存地址:

address[i] = base_address + data_type_size * i

其中 base_address 表示数组的起始地址,data_type_size 表示每个元素占用的空间大小。

可想而知,同时具有“连续的内存空间”和“相同类型的数据”两个限制的数组结构,通过下标查找数组中的元素能达到 \(O(1)\) 的时间复杂度。

插入、删除

同随机存储的高效不同,对一个数组做插入、删除操作是非常低效的。这里的低效体现在插入和删除元素之后,需要对数组中的其他元素做搬移操作,以保证数组元素的连续性。

在一个长度为 n 的数组中,假如要在第 k 个位置插入一个元素,这不是修改元素的操作,不能直接替换掉第 k 个元素,而是需要依次将第 k 个及之后的元素都往后挪一位,然后才能在第 k 个位置上存入这个元素。

数组删除元素时和插入元素类似,为了避免删除元素之后导致数组中间出现空洞,需要将删除位置之后的元素往前挪一位。

通过计算得知,插入、删除的最好时间复杂度为 \(O(1)\)、最坏时间复杂度为 \(O(n)\),平均时间复杂度为 \(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;
}

如上述代码,在字节对齐和分配内存的特性下,先后定义了 iarr 共占据了 8 个字节,表现为 {arr[0], arr[1], arr[2], i} 的形式,当循环到下标为 3 时,实际 arr[3] 指向的就是 i 所在地址,也就出现 arr[3] = i = 3,出现死循环。

相比较下,使用 Java 会更加安全,Java 不会把检查数组越界的工作丢给程序员来做,其本身就会做越界检查,如果出现错误则会抛出 域名yIndexOutOfBoundsException 异常,而不是出现死循环。

容器和数组的选择

这里说的容器指的是封装了数组的操作方法、并且支持动态扩容的容器类,比如 Java 中的 ArrayList 类。

与原生数组相比,ArrayList 拥有非常大的优势,但并不是所有地方都必须使用 ArrayList 而不使用数组,在某些地方使用数组更方便、效率更高。以下情况可以选择原生数组:

  • 追求极致性能选择原生数组。Java 的 ArrayList 不支持存储基本类型,而是存储基本类型封装后的对象,自动装箱、拆箱会有一定的性能消耗
  • 操作简单,仅使用原生功能选择原生数组。虽然 ArrayList 提供了非常多额外的功能,但也额外增加了风险,使用原生数组更简单便捷

为什么数组从 0 开始编号

这个问题可以通过数组的寻址公式来回答。

首先需要重新定义数组下标:下标不是只数组的第几个元素,而是指数组元素的偏移。

在使用时,使用 0 作为起始下标,数组使用下述的表达式作为寻址公式:

address[i] = base_address + data_type_size * i

如果使用 1 作为起始下标,将不能直接使用上面的寻址公式,而是需要修改如下:

address[i] = base_address + data_type_size * (i - 1)

在对比前后两个寻址公式之后,可以发现,使用 1 作为起始下标的寻址公式会比使用 0 作为起始下标的寻址公式多一个简单的减法指令。

对于非常底层的程序来说,即使只是多出一个简单的减法指令,也是一种性能的损耗,为了做到极致优化,选择 0 作为起始下标会更好。

当然还有一个历史原因,C 语言使用了 0 作为数组的起始下标,后续 Java、JavaScript 都纷纷仿效,这也算是为了统一,降低程序员的学习成本。

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。