学习笔记——前端算法与数据结构面试_基础

前言

一心想往高处爬,无何奈何地基垮。兜兜转转,还是回到了算法道路…饭还是要一口一口吃,拍拍灰尘,继续上路吧。

一、数组

数组是我们最早接触,也是最熟悉的数据结构。它简单,而又复杂。相对于链表,数组的优势在于它的查找速度,因为数组在内存中的地址分配总是连续的,可以直接根据 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]

而访问数组的方式,这里有一下三种

  1. for 循环
// 获取数组的长度
const len = arr.length
for (let i = 0; i < len; i++) {
    // 输出数组的元素值,输出当前索引
    console.log(arr[i], i)
}
  1. forEach 方法
    通过该方法传入函数两个参数,第一个参数为 value,第二个参数为 index
arr.forEach((item, index) => {
    // 输出数组的元素值,输出当前索引
    console.log(item, index)
})
  1. map 方法
    map 方法与 forEach 最大的不同,在于 map 方法可以返回一个全新的数组(并不会改变原来的数组)

作者的这段话讲的非常透彻,这里我就把它摘下来:

所以其实 map 做的事情不仅仅是遍历,而是在遍历的基础上“再加工”。当我们需要对数组内容做批量修改、同时修改的逻辑又高度一致时,就可以调用 map 来达到我们的目的:

const newArr = arr.map((item, index) => {
    // 输出数组的元素值,输出当前索引
    console.log(item, index)
    // 在当前元素值的基础上加1
    return item + 1
})

二、栈和队列

在 JavaScript 中,我们并不需要特意的去创建栈和队列,其自带非常便利的内置数组方法,让我们轻松实现栈和队列两类数据结构。

作者曰:

实际上,栈和队列作为两种运算受限的线性表,用链表来实现也是没问题的。只是从前端面试做题的角度来说,基于链表来实现栈和队列约等于脱裤子放屁(链表实现起来会比数组麻烦得多,做不到开箱即用),基本没人会这么干。这里大家按照数组的思路往下走就行了

内置数组方法

  1. push 方法-添加元素到数组尾部
const arr = [1, 2, 3]

// push 返回数组的长度

console.log(arr.push(7)) // 4   
console.log(arr) // [ 1, 2, 3, 7 ]
  1. pop 方法-删除数组尾部元素
const arr = [1, 2, 3]

// pop 返回刚刚被删除的元素

console.log(arr.pop(7)) // 3 
console.log(arr) // [ 1, 2 ]
  1. shift 方法-删除数组头部元素
const arr = [1, 2, 3]

// shift 返回刚刚被删除的元素

console.log(arr.shift(7)) // 1 
console.log(arr) // [ 2, 3 ]
  1. unshit 方法- 添加元素到数组头部
const arr = [1, 2, 3]

// unshift 返回数组的长度

console.log(arr.unshift(7)) // 4
console.log(arr) // [ 7, 1, 2, 3 ]
  1. 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

链表元素的添加

  1. 在链表尾部添加
    这个比较容易,我们直接创建一个新的结点 newNode,然后让 lastNode 指向 newNode 即可
// 创建新的结点
const newNode = new ListNode(1)

// 假设链表已经存在
lastNode.next = newNode
  1. 在链表中间插入
    这是一个关键的考点,也是链表最最基础的插入操作。
    我们假设已经存在结点 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 

五、数组与链表的区别

  1. 内存分配
    数组:在内存中是连续的。如果一个数组需要10个空间,内存恰好有10个空间,但并不是连续的,那么数组将无法被创建。
    链表:在内存中是离散的。只要内存中含有一个结点所需要的内存,便可以创建一个结点,增加链表的长度。
  2. 查找元素
    数组:根据索引 arr[3],即可直接查找到元素,时间复杂度 O(1)
    链表:无奈只能从头结点开始,向后遍历,直到 targetNode.val = val ,时间复杂度 O(n)
  3. 增加删除元素
    数组:不论是增加/删除一个元素,为了增加/删除一个元素空间,都要挪动后面的若干元素,当数组足够长时,花费的时间可想而知,时间复杂度 O(n)
    链表:相对就简单很多,只需要确认需要操作的结点的前驱结点,几步便可完成,时间复杂度 O(1)

算法道路并不好走,希望各位能继续坚持。

学习——大厂真题训练与解读_微软真题_修言

算法小册子出自这里,有兴趣的小伙伴可以购买一起学习哦

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值