数据结构之数组

什么是数组

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

下面是两个值得注意的概念:

1. 线性表(linear list)

线性表,即数据的逻辑结构是线性的。每个线性表中的数据最多只有向前和向后两个方向。典型的线性表有数组、链表、队列和栈。

和线性表相对应的就是非线性表,即数据之间不是简单的前后关系。如树、堆、图等。

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

这使数组具有一个堪称“杀手锏”的特性 ——“随机访问”,即可以通过下标随机访问数组元素。我们知道,计算机会给每个内存单元分配一个地址,然后通过地址来访问内存中的数据。当计算机需要访问数组中的某个元素时,就会通过下面的寻址公式,计算出存储该元素的内存地址:

a[i]_address = base_address + i * data_type_size

其中,data_type_size 是数组中每个元素的大小,base_address 是内存块的首地址。这样,就可以用 O(1) 的时间复杂度获取到下标为 i 的数组元素。

当然,有利就有弊,这一特性使得数组的很多操作变得低效,比如插入、删除操作,为了保证连续性,需要做大量的数据搬移工作。

低效的插入和删除

下面来看看,为什么数组的插入和删除操作是低效的,又有哪些改进方法呢?

1. 插入

假设数组长度为 n,要在第 k 位插入一个元素。那么就需要将第 k ~ n 位元素顺序后移,然后将新元素插入第 k 位。

如果 k = n - 1,则不需要再移动数据,此时时间复杂度为 O(1);如果 k = 0,则需要移动所有的数据,此时时间复杂度为 O(n)。用前一篇文章的分析方法(算法复杂度分析(下)),可知平均时间复杂度为:

(1 + 2 + ... + n) / n = O(n)

所以,数组的插入操作平均时间复杂度为 O(n),是一项低效的操作。

但是,如果数组中存储的数据没有任何规律,插入后不需要保持原来的顺序,则可以进行如下操作:

直接将第 k 位的数据搬移到数组元素最后,然后把新的元素直接放在第 k 位。

这样,时间复杂度就被降为了 O(1)。这个思想在快排中也会用到。

2. 删除

和插入操作类似,如果要删除第 k 位的元素,为了内存的连续性,也需要搬移数据,否则中间就会出现空洞,内存就不连续了。容易分析出,删除操作的平均时间复杂度也为 O(n)。

事实上,我们可以将多次删除操作集中在一起执行,先标记下已经删除的数据,然后在某些条件下(比如没有更多空间存储数据时),统一执行真正的删除操作,这样就大大减少了删除操作导致的数据搬移。

这种标记清除思想在其他地方也有所应用,比如垃圾回收中的标记清除算法。

数组越界问题

访问数组的本质是访问一段连续内存,只要数组通过偏移计算得到的内存地址都是可用的,那么程序就可能不会报任何错误。

很多计算机病毒也正是利用了代码中数组越界可以访问非法地址的漏洞来进行攻击,所以写代码时一定要警惕数组越界。

JavaScript 中的数组

作为一名前端,最后重点讲一讲 JavaScript 中的数组。

1. 不是在连续内存空间中存储同类型数据

JavaScript 中的数组是一种引用数据类型(也叫复杂数据类型),它其实是一种对象,继承于 Object,存储与堆内存中。

Array.prototype.__proto__ === Object.prototype    // true

JavaScript 中可以通过数组的 length 属性来获取其长度:

arr.length

而 C 中需要使用这种方法:

sizeof(arr) / sizeof(arr[0])

python 中则是:

len(arr)

JavaScript 中的数组本质上是 key 为 '0', '1', '2', ... 的对象,对应的 value 可以为任意值。这样的话,就可以很容易解释下面这种现象:

var arr = ['a', 1]
var key = { toString: function () { return '1' }}
key in arr    // true

2. 插入、删除的性能问题

很惭愧,学艺不精,这个话题暂时就不发表评论了,以免误人子弟。感兴趣的同学可以移步这里:https://stackoverflow.com/que...

3. 不会有数组越界问题

JavaScript 数组空间不足时,会自动进行扩容操作,不会出现数组越界问题。具体分析可以参考这里:https://zhuanlan.zhihu.com/p/...


本文是《数据结构与算法之美》的读书笔记,首发于微信公众号《代码写完了》

数据结构之数组

数据结构之数组

相关推荐