数据结构与算法-05-数组-为什么很多编程语言中数组都从0开始编号

本小节讲解了如下几个问题

  • 如何实现随机访问?
  • 数组的删除和插入操作的复杂度?
  • 为什么下标从0开始?
  • 严格控制数组的越界访问!
  • 容器和数组的关系?
  • JVM和数组有什么相同之处呢?

下面依次回答以上几个问题

如何实现随机访问?

大家都知道数组能够实现随机快速访问(指的是读);数组与其他的数据结构有什么区别呢?

数组的定义:数组(Array)是一种 线性表 数据结构。它用一组 连续的内存空间 ,来存储一组具有 相同类型 的数据。
线性表:线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。
线性表
非线性表:二叉树、堆、图等。之所以叫非线性,是因为,在非线性表中,数据之间并不是简单的前后关系。
非线性表

数组的删除和插入操作的复杂度?

插入数据x到位置i
读取位置i的数据

  • 有序数组的插入,当插入一个数据之后,要把插入位置之后的所有数据全部往后移动一个位置。
    插入:O(n)
    读取:O(1)

  • 无序数组的插入,当想把数据x插入到位置i;只需要将数据x插入到数组最后一个位,然后在与位置i的数据交换即可。
    插入:O(1)
    读取:O(1)

  • 删除操作

  1. 与插入类似
  2. 但是,当只是标记删除,并不移动其他数据呢?

比如数组 a[10] 中存储了 8 个元素:a,b,c,d,e,f,g,h。现在,我们要依次删除 a,b,c 三个元素。
删除操作
为了避免 d,e,f,g,h 这几个数据会被搬移三次,我们可以先记录下已经删除的数据。每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,我们再触发执行一次真正的删除操作,这样就大大减少了删除操作导致的数据搬移。

感受到了和JVM垃圾回收机制有没有什么相似之处呢?

为什么下标从0开始?

从数组存储的内存模型上来看,“下标”最确切的定义应该是“偏移(offset)”。前面也讲到,如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:

1
a[k]_address = base_address + k * type_size

但是,如果数组从 1 开始计数,那我们计算数组元素 a[k] 的内存地址就会变为:

1
a[k]_address = base_address + (k-1)*type_size

对比两个公式,我们不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运
算,对于 CPU 来说,就是多了一次减法指令。

当然,也有可能是因为历史原因。。

控制数组的越界访问!

这个不用说了吧,对于java来说,数组越界会进行越界检查。但是对于C等语言来说,数组越界就会产生未定义行为。

容器和数组的关系?

  1. java种ArrayList是一种不用定义大小的数组,也能够很方便的进行数据的插入和数据的删除。但是由于其长度的动态变化,所以当在知道数据大小的前提下,还是使用数组比较好。或者直接指定ArrayList的大小。
    同时ArrayList种的对象也只能够是封装好的Integer,Long等类型,
  2. Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
  3. 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
  4. 还有一个是我个人的喜好,当要表示多维数组时,用数组往往会更加直观。比如Object[][] array;而用容器的话则需要这样定义:ArrayList array。

JVM和数组有什么相同之处呢?

JVM的标记-整理算法:会先把垃圾进行标记,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。