该文章是本人在学习前端算法与数据结构面试:底层逻辑解读与大厂真题训练小册的笔记第一篇~
需要掌握的数据结构
- 数组
- 栈
- 队列
- 链表
- 树(二叉树)
数组
何为数组?
必要条件:存储在连续的内存空间里
数组的创建
javascript
const arr = [1, 2, 3, 4]
创建指定长度的空数组,例如创建一个长度为7的空数组:
javascript
const arr = new Array(7)
创建指定长度的全为1的数组,例如创建一个长度为7的全为1的数组:
javascript
const arr = new Array(7).fill(1)
数组的遍历
-
【推荐】for循环(从性能上看,for 循环遍历起来是最快的)
javascript// 获取数组的长度 const len = arr.length for(let i=0;i<len;i++) { // 输出数组的元素值,输出当前索引 console.log(arr[i], i) }
-
forEach 方法
javascriptarr.forEach((item, index)=> { // 输出数组的元素值,输出当前索引 console.log(item, index) })
-
map方法
map 方法在调用形式上与 forEach 无异,区别在于 map 方法会根据你传入的函数逻辑对数组中每个元素进行处理、进而返回一个全新的数组。
javascriptconst newArr = arr.map((item, index)=> { // 输出数组的元素值,输出当前索引 console.log(item, index) // 在当前元素值的基础上加1 return item + 1 })
这段代码就通过 map 来返回了一个全新的数组,数组中每个元素的值都是在其现有元素值的基础上+1后的结果。
初始化一个二维数组
推荐大家采纳的二维数组初始化方法非常简单(而且性能也不错)。直接用一个 for 循环来解决:
*for 循环中,每一次迭代我们都通过"[]"来创建一个新的数组,这样就不会出现使用fill([])导致的指向问题了。
javascript
const len = arr.length
for(let i=0;i<len;i++) {
// 将数组的每一个坑位初始化为数组
arr[i] = []
}
栈和队列
在 JavaScript 中,栈和队列的实现一般都要依赖于数组,所以可以将栈和队列看作是"特别的数组"。
数组、栈、队列三者之间的区别在于 ------ 对数组的增删操作有不同的限制。
数组 ------ 灵活删减
数组中增加元素的三种方法:
unshift(a) 将元素a添加到数组的头部
push(a) 将元素a添加到数组的尾部
splice(index, num, a) 添加元素到数组的任何位置
第一个入参是起始的索引值,第二个入参表示从起始索引开始需要删除的元素个数,第三个位置开始的入参,都代表着需要添加到数组里的元素的值。
数组中删除元素的三种方法:
- shift() 删除数组头部的元素
- pop() 删除数组尾部的元素
- splice(index, num) 删除从index位置开始num个元素
栈(Stack)------只用 pop 和 push 完成增删的"数组" *堆放东西
栈是一种后进先出(LIFO,Last In First Out)的数据结构。
栈有两个特征:
- 只允许从尾部添加元素 => 只能用 push 来添加元素
- 只允许从尾部移除元素 => 只能用 pop 来移除元素
队列(Queue)------只用 push 和 shift 完成增删的"数组" *排队
队列是一种先进先出(FIFO,First In First Out)的数据结构。
队列有两个特征:
- 只允许从尾部添加元素 => 只能用 push 来添加元素
- 只允许从头部移除元素 => 只能用 shift 来移除元素
链表
链表和数组相似,它们都是有序的列表、都是线性结构(有且仅有一个前驱、有且仅有一个后继)。不同点在于,链表中,数据单位的名称叫做"结点",而结点的分布,在内存中可以是离散的。
例如,一个内容为1->2->3->4->5的链表,在内存中的形态可以是散乱如下的:
在链表中,每一个结点的结构都包括了两部分的内容:数据域和指针域(指向下一个结点)。
javascript{ // 数据域 val: 1, // 指针域,指向下一个结点 next: { val:2, next: ... } }
*要想访问链表中的任何一个元素,我们都得从起点结点开始,逐个访问 next,一直访问到目标结点为止。
有时还会设定一个 head 指针来专门指向链表的开始位置:
链表节点的创建
创建列表结点,我们需要一个构造函数:
javascript
function ListNode(val) {
this.val = val;
this.next = null;
}
然后我们就可以使用构造函数创建结点。传入val作为参数,并指定next即可
javascript
const node = new ListNode(1)
node.next = new ListNode(2)
这样我们就创建出了一个数据域值为1,next 结点数据域值为2的链表结点:
链表元素的添加
要想完成这个动作,我们需要变更的是前驱结点 和目标结点的 next 指针指向。
javascript
// 如果目标结点本来不存在,那么记得手动创建
const node3 = new ListNode(3)
// 把node3的 next 指针指向 node2(即 node1.next)
node3.next = node1.next
// 把node1的 next 指针指向 node3
node1.next = node3
链表元素的删除
删除的标准是:在链表的遍历过程中,无法再遍历到某个结点的存在。
其实也就是让这个结点脱离这个链表的关系链。
注意:在涉及链表删除操作的题目中,重点不是定位目标结点,而是定位目标结点的前驱结点。
在js中的数组与链表
*JS 数组未必是真正的数组 JS 数组和常规数组的不同:
- 在js中,若在一个数组中只定义了一种类型元素,那么对应的就是连续内存
- 但如果在数组中定义了不同类型的元素,它对应的就是一段非连续的内存。此时,JS 数组不再具有数组的特征 ,其底层使用哈希映射分配内存空间,是由对象链表来实现的。
*链表的特点
【优点】高效的增删操作:添加和删除元素都不需要挪动多余的元素。O(1)
【缺点】麻烦的访问操作:当我们试图读取某一个特定的链表结点时,必须遍历整个链表来查找它。O(n) 而在数组中可以通过直接访问索引实现一步访问:O(1)
因此涉及到数组与链表两种数据结构选型时,要明确两者的特性:
-
链表的插入/删除效率较高,而访问效率较低;
-
数组的访问效率较高,而插入效率较低。
树
二叉树结构
- 它可以没有根结点,作为一棵空树存在。
- 如果它不是空树,那么必须由根结点、左子树和右子树组成,且左右子树都是二叉树。
- 且与普通的树不同,二叉树中会严格区分左右子树。
创建二叉树
创建二叉树,我们需要一个构造函数:
- 数据域
- 左侧子结点(左子树根结点)的引用
- 右侧子结点(右子树根结点)的引用
javascript
// 二叉树结点的构造函数
function TreeNode(val) {
this.val = val;
this.left = null;
this.right = null;
}
当你需要新建一个二叉树结点时,直接调用构造函数、传入数据域的值val就行了:
javascript
const node = new TreeNode(1)
以这个结点为根结点,我们可以通过给 left/right 赋值拓展其子树信息,延展出一棵二叉树。