数组是我们开发中最常用的数据结构之一,在大部分编程语言中数组的下标都是从0开始的,为什么不是从1开始呢?
1,数组如何实现随机访问?
首先说下数组的定义:数组是一种线性的数据结构,它用一组连续的内存来存储相同类型的数据。
数据的定义涉及到两个关键字,线性结构和连续内存,下面分析下这两个关键字
1.1 线性结构
线性结构就是数据排成像一条线一样的数据结构,常见的线性数据结构有数组、链表、队 列、栈等也是线性表结构。
1.2 连续内存
拿一个长度为10的int类型的数组int[] a = new int[10]来举例。在我画的这个图中,计算机给数组a[10],分配了一块连续内存空间1000~1039,其中,内存块的 首地址为base_address = 1000。
计算机会给每个内存分配单元分配一个内存地址,计算机通过地址来访问内存中的数据。当计算机需要随机访问数组中的元素时,它会通过下面寻址公式,计算出存储元素的内存地址:
a[i]_address = base_address + i * data_type_size(每个元素的大小)
通过寻址公式,数组实现来随机访问元素,随机访问元素的时间复杂的是O(1)。
2,低效的删除与插入
数组为了保持内存的连续性,会导致删除与插入元素操作比较低效。我们现在就来分析一下,究竟为什么会导致低效?
我们先来看插入操作:
假设有一个长度为n的数组,我们现在需要将一个元素插入到数组中第k个位置。为了将第k个位置空出来给新插入的元素,我们需要将k~n位置的所有元素全部往后移一位,时间复杂的是O(n);
我们再来看删除操作:
跟插入操作类似,如果我们需要删除第k个元素,为了保持内存的连续性,需要将元素全部往前移动一位,时间复杂的同样也是O(n);
3,数组删除性能提升
在某些场景上,我们不一定非得追求数组的连续性。如果我们需要删除多个元素,将这几次删除操作合并在一起,删除的效率就会得到提升。
举个例子:
假设数组a[10]中存储了8个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除a,b,c三个元素,为了避免d,e,f,g,h这几个数据会被搬移三次,我们可以先记录下已经删除的数据。每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数 组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。
实际上,这就是JVM标记清除垃圾回收算法的核心思想。
4,为什么不是从0开始
原因1:
从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用a来表示数组的首地址,a[0]就是偏移为0的位置,也就是首地 址,a[k]就表示偏移k个type_size的位置,所以计算a[k]的内存地址只需要用这个公式:
a[k]_address = base_address + k * type_size
但是,如果数组从1开始计数,那我们计算数组元素a[k]的内存地址就会变为: a[k]_address = base_address + (k-1)*type_size
对比两个公式,我们不难发现,从1开始编号,每次随机访问数组元素都多了一次减法运算,对于CPU来说,就是多了一次减法指令。 数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从0开始编号,而不是从1开始。
原因2:
C语言设计者用0开始计数数组下标,之后的Java、JavaScript等高级语言都效仿了C语言,或者说,为了在一定程度上减少C语言程序员学习Java的学习成本,因此 继续沿用了从0开始计数的习惯。实际上,很多语言中数组也并不是从0开始计数的,比如Matlab。甚至还有一些语言支持负数下标,比如Python。