跳至主要內容

2.1 数组


2.1 数组

数组的定义

定义

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

但在 JavaScript 里,数组中可以保存不同类型的值(大多数语言都没这个能力)。但我们还是要遵守最佳实践,别这么做。

// 一维数组:
// 数组的每一个元素是一个数据类型
[1, 2, 3]

// 二维数组:
// 数组的每一个元素是一个一维数组
[["a", "b", "c"], [1, 2, 3], 123]

// 三维数组:
// 数组的每一个元素是一个二维数组
[
  [
    ["a", "b", "c"],
    [1, 2, 3],
  ],
  [
    ["a", "b", "c"],
    [1, 2, 3],
  ],
]

我们还可以从两个方面来解释一下数组的定义。

  • 线性表
  • 连续的内存空间

线性表与非线性表

定义

线性表(Linear List) 就是数据排成像一条线一样的结构,线性表上的数据元素都是相同类型。每个线性表上的数据最多只有前和后两个方向。

数组、链表、队列、栈都是是线性表结构

线性表有两种存储结构:「顺序存储结构」和「链式存储结构」。

其中,「顺序存储结构」是指占用的内存空间是连续的,相邻数据元素之间,物理内存上的存储位置也相邻。

数组也是采用了顺序存储结构,并且存储的数据都是相同类型的。

与它相对立的是非线性表,比如二叉树、堆、图等。

在非线性表中,数据之间并不是简单的前后关系。

数组的操作

数据结构的操作一般涉及到增、删、改、查共 4 种情况,下面我们一起来看一下数组的这 4 种基本操作。

1. 访问和查找元素

因为数组有连续的内存空间和相同类型的数据,所以数组支持 “随机访问”

但这两个限制也让数组的很多操作变得非常低效,比如在数组中删除、插入一个数据,为了内存数据的保证连续性,就需要做大量的数据搬移工作。

在面试的时候,面试官常常会问数组和链表的区别?很多人都回答说,“链表适合插入、删除,时间复杂度 O(1);数组适合查找,查找时间复杂度为 O(1)”。实际上,这种表述是不准确的。

数组适合查找操作,但是查找的时间复杂度并不为 O(1)。即便是排好序的数组,用二分查找,时间复杂度也是 O(logn)

所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)

2. 修改元素

  • 修改指定索引位置的元素 array.splice(index, 1, item)
    let myArray3 = [1, 2, 3, 4, 5, 6];
    // 修改 索引 1 的位置的元素为 AA
    myArray2.splice(1, 1, 'AA');
    console.log(myArray3); //--> [1, "AA", 3, 4, 5, 6]
    
  • 修改指定索引位置的几个元素 array.splice(index, number, item)
    let myArray4 = [1, 2, 3, 4, 5, 6, 7];
    // 在 索引 2 的位置起,修改两个元素为 AA BB
    myArray2.splice(2, 2, 'AA', 'BB');
    console.log(myArray3); //--> [1, 2, "AA", "BB", 5, 6, 7]
    

改变元素的操作跟访问元素操作类似,访问操作不依赖于数组中元素个数,因此,「改变元素」的时间复杂度为 O(1)

3. 插入元素

  • 添加一个元素到数组的最后位置 array.push(item)
  • 在数组首位插入一个元素 array.unshift(item)
  • 在指定索引位置插入元素 array.splice(index, 0, item)

    splice() 第二个参数为 0 时,表示插入数据。

    let myArray = [1, 2, 3];
    // 在 索引 0 的位置,插入 A
    myArray.splice(0, 0, 'A');
    console.log(myArray); //--> ['A', 1, 2, 3]
    

假设数组的长度为 n,将一个数据插入到数组中的第 k 个位置。为了把第 k 个位置腾给新来的数据,要将第 k ~ n 这部分的元素都顺序地往后挪一位。

  • 如果在数组的末尾插入元素,不需要移动数据,时间复杂度为 O(1)
  • 但如果在数组的开头插入元素,那所有的数据都要依次往后移动一位,所以最坏时间复杂度是O(n)
  • 因为每个位置插入元素的概率一样,所以平均情况时间复杂度为(1 + 2 + … + n) / n = O(n)

如果数组中的数据是有序的,在插入新元素时就必须搬移 k 之后的数据。但是,如果数组中存储的数据并没有任何规律,将某个新元素插入到第 k 个位置时,为了避免大规模的数据搬移,有一个简单的办法就是,直接将第 k 位的数据搬移到数组元素的最后,把新的元素直接放入第 k 个位置。

例如:数组 a 中存储了如下 5 个元素:a,b,c,d,e。要将元素 x 插入到第 3 个位置。只需将 c 放入到 a[5],将 a[2]赋值为 x 即可。最后数组中的元素如下: a,b,x,d,e,c。

利用这种处理技巧,在特定场景下,在第 k 个位置插入一个元素的时间复杂度就会降为 O(1)。这个处理思想在快排中也会用到。

4. 删除元素

  • 删除数组最后的元素 array.pop()
  • 删除数组首位的元素 array.shift()
  • 删除指定索引位置的元素 array.splice(start, deleteCount) 例如:
    let myArray2 = [1, 2, 3, 4, 5];
    // 删除索引 3 位置起,2 个元素
    myArray2.splice(3, 2);
    console.log(myArray2); //--> [1, 2, 3]
    

跟插入数据类似,如果我们要删除第 k 个位置的数据,为了内存的连续性,也需要搬移数据,不然中间就会出现空洞,内存就不连续了。

和插入类似:

  • 如果删除数组末尾的数据,则最好情况时间复杂度为 O(1)
  • 如果删除开头的数据,则最坏情况时间复杂度为 O(n)
  • 平均情况时间复杂度也为 O(n)

实际上,在某些特殊场景下,并不一定非得追求数组中数据的连续性。如果每次的删除操作并不是真正地搬移数据,只是记录数据已经被删除。当数组没有更多空间存储数据时,再触发执行一次真正的删除操作,就可以大大减少了删除操作导致的数据搬移。这也是JVM 标记清除垃圾回收算法的核心思想。

相关题目

数组操作

题号标题题解标签难度
189轮转数组open in new window[✓]数组 数学 双指针
66加一open in new window[✓]数组 数学
724寻找数组的中心下标open in new window[✓]数组 前缀和
485最大连续 1 的个数open in new window[✓]数组
238除自身以外数组的乘积open in new window[✓]数组 前缀和

二维数组

题号标题题解标签难度
498对角线遍历open in new window[✓]数组 矩阵 模拟
48旋转图像open in new window[✓]数组 数学 矩阵
73矩阵置零open in new window[✓]数组 哈希表 矩阵
54螺旋矩阵open in new window[✓]数组 矩阵 模拟
59螺旋矩阵 IIopen in new window[✓]数组 矩阵 模拟
289生命游戏open in new window[✓]数组 矩阵 模拟