数组
作为最简单,最基础的一个数据结构,大多数语言都天然地对数组有着原生的表达,Javascript亦然。
“开箱即用”,而不必自行实现,非常方便。
首先我们需要知道: JS 数组未必是真正的数组
在大多数的计算机语言中,数组都对应着一段连续的内存。如果我们想要在任意位置删除一个元素,那么该位置往后的所有元素,都需要往前挪一个位置;相应地,如果要在任意位置新增一个元素,那么该位置往后的所有元素也都要往后挪一个位置。
我们假设数组的长度是 n,那么因增加/删除操作导致需要移动的元素数量,就会随着数组长度 n 的增大而增大,呈一个线性关系。所以说数组增加/删除操作对应的复杂度就是 O(n)。
但 JS 中不一定是。
JS比较特别。如果我们在一个数组中只定义了一种类型的元素,比如:
const arr = [1,2,3,4]
它是一个纯数字数组,那么对应的确实是连续内存。
但如果我们定义了不同类型的元素:
const arr = ['haha', 1, {a:1}]
它对应的就是一段非连续的内存。此时,JS 数组不再具有数组的特征,其底层使用哈希映射分配内存空间,是由对象链表来实现的。
所以说“JS 数组未必是真正的数组”。
在各大教材(包括百科词条)对数组的定义中,都有一个“存储在连续的内存空间里”这样的必要条件。
这同时是前端面试题中,数组和链表辨析的答案,我们要意识到 JS 数组和常规数组的不同,相对于数组来说,链表有个明显的有点,就是添加和删除元素都不需要挪动多余的元素。
Tips:要对数组格外走心,毕竟后面需要它帮忙的地方会非常多。
数组的创建:
最多的创建方式:
const arr = [ 1, 2, 3 ]
但是在算法题中,很多时候我们初始化一个数组时,并不知道它内部元素的情况,这种场景下,推荐大家使用new的方式初始化数组
const arr = new Array()
// 等价于
const arr = []
不过我们使用构造函数,可不是为了创建空数组这么无聊。
我们需要它的时候,往往是因为我们有创造指定长度的空数组这样的需求。需要多长的数组就给它传多大的参数。
const arr = new Array(7)
// 创建一个指定长度为7的数组
在一些场景还需要我们创建一个指定长度,同时每一个元素的值也都确定的数组。这时我们可以调用fill方法,假设需求是每一个坑里都填上一个1,只需给它fill一个1:
const arr = (new Array(7)).fill(1)
// [1,1,1,1,1,1,1]
数组的访问和遍历:
访问数组中的元素,我们直接在中括号中指定其索引即可:
arr[0] // 访问索引下标为0的元素
而遍历数组,这个方法就多了,不过目的往往都是一致的(访问数组中的每一个元素,并且知道当前元素的索引)。这里我们介绍三个方法:
1.for循环
这是最基础的操作。我们可以通过循环数组的下标,来依次访问每个值:
// 获取数组的长度
const len = arr.length
for(let i=0;i<len;i++) {
// 输出数组的元素值,输出当前索引 console.log(arr[i], i)
}
2.forEach方法
通过区forEach方法中传入函数的第一个入参和第二个入参,我们也可以取到数组每个元素的值及其对应索引:
arr.forEach((item, index)=> {
// 输出数组的元素值,输出当前索引
console.log(item, index)
})
3.map方法
map方法在调用形式上与forEach无异,区别于map方法会根据你传入的函数逻辑对数组的每一个元素进行处理,进而返回一个全新的数组。
所以其实map做的事情不仅仅是遍历,而是在遍历的基础上再加工。当我们需要对数组内容做批量修改,同时修改逻辑又高度一致时,就可以调用map来达到我们的目的:
const newArr = arr.map((item, index)=> {
// 输出数组的元素值,输出当前索引
console.log(item, index)
// 在当前元素值的基础上加1
return item+1
})
小建议:没有特殊的需求,统一使用for循环来实现遍历。因为从性能上看,for循环遍历起来是最快的。
二维数组
二维数组的特点:从形状上看,相对于一维数组一条“线”一般的布局,二维数组更像是一个“面”。拿咱们这个例子来说,这里的二维数组逻辑分布图就宛如一个正方形。当然啦,如果我们稍微延长一下其中的一边,它也可以是一个矩形。
在数学中,形如这样长方阵列排列的复数或实数集合,被称为“矩阵”。因此二维数组的别名就叫“矩阵”。
必须要记住“矩阵”和“二维数组”之间的等价关系。在算法题目中,见到“矩阵”时,能够立刻反射出它说的是二维数组,不被别名整懵逼,这就够了。
二维数组的初始化:
const arr =(new Array(7)).fill([])
以上的方式对吗?乍一看,没什么毛病。
但是当你想修改某一个坑里的数组的值的时候,你就会发现问题。
正确的初始化一个二维数组的方式:
const len = arr.length
for(let i=0;i<len;i++) {
// 将数组的每一个坑位初始化为数组 arr[i] = []
}
二维数组的访问:
访问二维数组和访问一维数组差别不大,区别在于我们现在需要的是两层循环。
一维数组用 for 循环遍历只需一层循环,二维数组是两层,三维数组就是三层。依次类推,N 维数组需要 N 层循环来完成遍历。
这远不是数组的全部,目前仅围绕数组最基本的操作进行介绍,在javascript数据结构中,数组几乎是基石一般的存在。
栈和队列
在javascript中,栈和队列的实现一般都要依赖于数组,“特别的数组”。
(注:实际上,栈和队列作为两种运算受限的线性表,用链表来实现也是没问题的。只是从前端面试做题的角度来说,基于链表来实现栈和队列约等于脱裤子放屁(链表实现起来会比数组麻烦得多,做不到开箱即用),基本没人会这么干。这里大家按照数组的思路往下走就行了)
作为有经验的前端,大家对javascript数组的操作恐怕都不陌生。
但我们今天讲的栈和队列,两者的区别在于,它们各自对数组的增删操作有着不一样的限制。因此我们先熟悉一下javascript数组中的增删操作具有什么样的特性,对应的方法有哪些?
增加:
unshift 方法-添加元素到数组的头部
push 方法-添加元素到数组的尾部
splice 方法-添加元素到数组的任何位置
删除:
shift 方法-删除数组头部的元素
pop 方法-删除数组尾部的元素
splice 方法-删除数组任意位置的元素
栈(Stack)——只用 pop 和 push 完成增删的“数组”
栈是一种后进先出(LIFO,Last In First Out)的数据结构。
队列(Queue)——只用 push 和 shift 完成增删的“数组”
队列是一种先进先出(FIFO,First In First Out)的数据结构。
阅读掘金小册的《前端算法与数据结构面试:底层逻辑解读与大厂真题训练》所获。富有的朋友们值得购买一读。