前端算法与数据结构——数组/栈/队列

数组

作为最简单,最基础的一个数据结构,大多数语言都天然地对数组有着原生的表达,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

    // 输出数组的元素值,输出当前索引 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

    // 将数组的每一个坑位初始化为数组 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)的数据结构。

阅读掘金小册的《前端算法与数据结构面试:底层逻辑解读与大厂真题训练》所获。富有的朋友们值得购买一读。

你可能感兴趣的:(前端算法与数据结构——数组/栈/队列)