前言
一心想往高处爬,无何奈何地基垮。兜兜转转,还是回到了算法道路…饭还是要一口一口吃,拍拍灰尘,继续上路吧。
一、数组
数组是我们最早接触,也是最熟悉的数据结构。它简单,而又复杂。相对于链表,数组的优势在于它的查找速度,因为数组在内存中的地址分配总是连续的,可以直接根据 index 来实现查找功能,也正是由于这个特点,使其增加删除元素的速度大大降低。
数组的创建
我们平时用的最多的创建方式
const arr = [1, 2, 3, 4]
这种方式虽然便利,但我们平时在做算法题的时候,大多时候并不知道数组内有多少个元素、以及元素是什么,于是便使用 new 来创建数组
const arr = new Array()
// 等同于 arr = []
当然我们也可以直接为数组指定长度
const arr = new Array(7)
在某些场景下,可能会需要给数组中的元素默认值,这时我们可以调用 fill 函数
// 这里是令每个元素都为1
const arr = (new Array(7)).fill(1)
二维数组
这是一维数组
const arr = [1, 2, 3, 4]
这是二维数组
const arr = [
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
[1, 2, 3],
[1, 2, 3]
]
直观的来看,最简单二维数组莫过于
const arr = [[]]
聪明的小伙伴们肯定想到了用 fill 函数的方法,将数组填充进元素,这个方法真的可行吗
const arr =(new Array(7)).fill([])
乍一看好像是没什么毛病
但如果我们想修改其中某一个值时
arr[0][0] = 1
我们发现所有的元素都被设置为了1 ?!
问题出在 fill 函数的工作机制,它传进值是参数的引用,所以每一个元素都指向同一个地址。
所以本着安全、最简单的方式,我们采用 for 循环来创建一个二维数组,可能小伙伴们会觉得很 low,但我们学习并使用数据结构,不就为了让代码更完美、性能更好吗
const arr = new Array(3)
const len = arr.length
for (let i = 0; i < len; i++) {
// 将数组的每一个坑位初始化为数组
arr[i] = []
}
数组的访问与遍历
访问数组中的元素,我们一般都直接在括号中指定 index
arr[0]
而访问数组的方式,这里有一下三种
- for 循环
// 获取数组的长度
const len = arr.length
for (let i = 0; i < len; i++) {
// 输出数组的元素值,输出当前索引
console.log(arr[i], i)
}
- forEach 方法
通过该方法传入函数两个参数,第一个参数为 value,第二个参数为 index
arr.forEach((item, index) => {
// 输出数组的元素值,输出当前索引
console.log(item, index)
})
- map 方法
map 方法与 forEach 最大的不同,在于 map 方法可以返回一个全新的数组(并不会改变原来的数组)
作者的这段话讲的非常透彻,这里我就把它摘下来:
所以其实 map 做的事情不仅仅是遍历,而是在遍历的基础上“再加工”。当我们需要对数组内容做批量修改、同时修改的逻辑又高度一致时,就可以调用 map 来达到我们的目的:
const newArr = arr.map((item, index) => {
// 输出数组的元素值,输出当前索引
console.log(item, index)
// 在当前元素值的基础上加1
return item + 1
})
二、栈和队列
在 JavaScript 中,我们并不需要特意的去创建栈和队列,其自带非常便利的内置数组方法,让我们轻松实现栈和队列两类数据结构。
作者曰:
实际上,栈和队列作为两种运算受限的线性表,用链表来实现也是没问题的。只是从前端面试做题的角度来说,基于链表来实现栈和队列约等于脱裤子放屁(链表实现起来会比数组麻烦得多,做不到开箱即用),基本没人会这么干。这里大家按照数组的思路往下走就行了
内置数组方法
- push 方法-添加元素到数组尾部
const arr = [1, 2, 3]
// push 返回数组的长度
console.log(arr.push(7)) // 4
console.log(arr) // [ 1, 2, 3, 7 ]
- pop 方法-删除数组尾部元素
const arr = [1, 2, 3]
// pop 返回刚刚被删除的元素
console.log(arr.pop(7)) // 3
console.log(arr) // [ 1, 2 ]
- shift 方法-删除数组头部元素
const arr = [1, 2, 3]
// shift 返回刚刚被删除的元素
console.log(arr.shift(7)) // 1
console.log(arr) // [ 2, 3 ]
- unshit 方法- 添加元素到数组头部
const arr = [1, 2, 3]
// unshift 返回数组的长度
console.log(arr.unshift(7)) // 4
console.log(arr) // [ 7, 1, 2, 3 ]
- splice 方法-添加元素到数组的任何位置、删除数组任意位置的元素
const arr = [1, 2, 3, 4, 5]
// 第一个参数: 起始的索引值
// 第二个参数: 需要删除元素的个数
// 第三个以及后面的参数: 需要添加的元素
// splice 返回由被删除元素所组成的数组
console.log(arr.splice(1, 2)) // [ 2, 3 ]
console.log(arr) // [ 7, 1, 2, 3 ]
console.log(arr.splice(1, 0, 8, 9)) // []
console.log(arr) // [ 1, 8, 9, 4, 5 ]
console.log(arr.splice(1, 0, [1, 2, 3])) // []
console.log(arr) // [ 1, [ 1, 2, 3 ], 8, 9, 4, 5 ]
栈
栈(Stack)是一种后进先出(LIFO,Last In First Out)的数据结构。(只能 尾部进 尾部出)
场景(便于理解):往箱子里放书拿书(最后放的书 在最上面,最先被拿出来)
运用内置数组方法
- 入栈——push(对数组尾部操作)
- 出栈——pop(对数组尾部操作)
队列
队列(Queue)是一种先进先出(FIFO,First In First Out)的数据结构。(只能 尾部进 头部出)
场景(便于理解):超市排队付账(先到的人 先付钱,然后溜溜梅)
运用内置数组方法
- 入队——push(对数组尾部操作)
- 出队——shift(对数组头部操作)
三、链表
相对于数组的连续储存,链表在内存中是离散储存的,通过散落在内存中的各各结点连接而成。
可地址不同的结点通过什么连接呢?
每个结点由数据和指针组成
{
// 数据
val: 1,
// 指针
next: {
val: 2
next: ...
}
}
链表结点的创建
创建链表结点,我们需要一个构造函数,并调用它
function ListNode(val) {
this.val = val;
this.next = null;
}
const node1 = new ListNode(1)
const node2 = new ListNode(1)
// 这样就可以连接两个结点
node1.next = node2
链表元素的添加
- 在链表尾部添加
这个比较容易,我们直接创建一个新的结点 newNode,然后让 lastNode 指向 newNode 即可
// 创建新的结点
const newNode = new ListNode(1)
// 假设链表已经存在
lastNode.next = newNode
- 在链表中间插入
这是一个关键的考点,也是链表最最基础的插入操作。
我们假设已经存在结点 node1、结点 node2,将新结点 node3 插入到 node1 与 node2 中间。
// 第一步 创建新结点 node3
const node3 = new ListNode(3)
// 第二步 将 node3.next 指向 node2
node3.next = node1.next
// 第三步 将node1.next 指向 node3
node1.next = node3
可能有的小伙伴们会对第二步有些疑问,为什么不能直接
node3.next = node2
由于链表是通过结点相连的,我们并不能像数组那样,直接通过索引就能拿到。想要得知当前结点,就必须得知道其前驱结点。这里前驱结点 node1 的 next 就指向 node2 ,可拿到 node2
node1.next === node2
链表元素的删除
这跟数组可不一样,我们直接拿到想要深处的结点,是无法完成删除操作的。我们必须拿到该结点的前驱结点,让前驱结点的 next 指向该结点的后继结点。
// 假设我们我们已经有三个结点 node1 node2 node3
node1.next = node2.next
五、数组与链表的区别
- 内存分配
数组:在内存中是连续的。如果一个数组需要10个空间,内存恰好有10个空间,但并不是连续的,那么数组将无法被创建。
链表:在内存中是离散的。只要内存中含有一个结点所需要的内存,便可以创建一个结点,增加链表的长度。 - 查找元素
数组:根据索引 arr[3],即可直接查找到元素,时间复杂度 O(1)
链表:无奈只能从头结点开始,向后遍历,直到 targetNode.val = val ,时间复杂度 O(n) - 增加删除元素
数组:不论是增加/删除一个元素,为了增加/删除一个元素空间,都要挪动后面的若干元素,当数组足够长时,花费的时间可想而知,时间复杂度 O(n)
链表:相对就简单很多,只需要确认需要操作的结点的前驱结点,几步便可完成,时间复杂度 O(1)
算法道路并不好走,希望各位能继续坚持。