为什么数组从0开始编号

234 阅读4分钟

数组对一个程序员来说再熟悉不过了,几乎所有的编程语言都有数组,它是最基本的数据结构之一。

刚开始学数组的时候,总是很纳闷,为什么它从0开始编号,而不是从更符合我们思维习惯的1开始呢?带着这个问题往下看。

什么是数组

在编程的过程中,我们经常要用到数组,但是你能够用专业的词汇来描述什么是数组吗?

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

线性表

线性表 (linear list) 是指具有n个相同特性的数据元素组成的有限序列,你可以理解为“把所有数据用一根线串起来,再存储到物理空间中”,从图中可以明显的看出它最多只有前和后两个方向(节点)。

1G0424H3-1.gif

除了数组,链表、队列和栈也是线性表结构(线性表既可以顺序存储,也可以链式存储)

非线性表

与线性表对应,非线性表是指一个节点元素可能有多个直接前驱和多个直接后继,比如树和图

6ebf42641b5f98f912d36f6bf86f6569.jpg

连续的内存空间和相同的数据类型

同一个数组中的数据类型都是一样的,并且它们连续存储在内存中,所以我们可以对数组内容进行随机访问。随机访问这个过程是怎么实现的呢?

计算机会给每个内存单元分配一个地址,通过地址来访问内存中的数据。当它需要随机访问数组中的某个元素时,首先会通过寻址公式,计算出该元素存储的内存地址,然后对其进行读取操作,寻址公式为

    a[i]_address = base_address + i * data_type_size    //其中i表示数组中第几个元素

我们拿一个长度为 10 的 int 类型的数组 int[] a = new int[10]来举例。在这个图中,计算机给数组 a[10]分配了一块连续内存空间 1000~1039,其中,内存块的首地址为 base_address = 1000,因为存储的是整数,所以data_type_size = 4。根据公式,我们很容易就能得到这个数组中某个元素在内存中的地址。

98df8e702b14096e7ee4a5141260cdc4.jpg

从内存上看,我们可以理解为什么大部分编程语言中的数组下标是0开始,更准确的讲它不是下标而是代表偏移(offset) 。如果用a来表示数组的首地址,a[0]是它偏移0的位置(就是首地址),a[2]代表是偏移2 * data_type_size的位置,也就是第三个元素的地址,a[i]就表示偏移i * data_type_size的位置。如果从1开始编号的话,那么计算数组a[i]的内存地址公式就会变为

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

对于CPU来说,多做了一次减法指令。

还有一个原因就是历史遗留问题,因为C语言使用0开始计数数组下标,后面的Java、C#等语言也都效仿了C语言,继续沿用该习惯.但现在有的语言也不是从0开始计数,比如Lua,Matlab等.(部分内容及图片参考王争老师的数据结构与算法之美)

数组的增删查操作

查找

前面已经讲过,可以通过下标访问数组中的元素,由于它是连续存储的,访问的时间复杂度是O(1).

添加

假设我们有一个数组,长度为n,要在它第k个位置上插入数据,要进行什么操作呢?为了空出第k个位置给新插入的数据,k~n这部分的数据都需要往后挪一位,那么它的平均时间复杂度为O(n).

1516809-20181023201007357-1981279990.gif

删除

删除操作于添加类似,为了保持内存的连续,不出现空洞,需要将删除元素后面的元素向前移,平均时间复杂度也为O(n)

1516809-20181023205229330-256224409.gif

如果对时间复杂度的概念不了解,可以查看算法复杂度分析

总结

image-20210919235909853.png