之前有简单接触过数据结构,概念性的问题直接跳过,重点记录刷题过程中比较常用且重要的思路和知识点,比较突出个人薄弱的部分
数组
一维数组
初始化
const arr = new Array()
const arr = (new Array(7)).fill(1)
遍历
map,forEach,for循环
从性能上看,for 循环遍历起来是最快的,推荐使用
二维数组
初始化
const arr =(new Array(7)).fill([])
注意,如果入参类型是引用型,那么fill在填充坑位时填充的其实就是入参的引用。因此当你修改第0行第0个元素的值时,第1-6行的第0个元素的值也都会跟着发生改变。
直接用一个 for 循环来解决
const len = arr.length
for(let i=0;i<len;i++) {
// 将数组的每一个坑位初始化为数组
arr[i] = []
}
遍历
N 维数组需要 N 层循环来完成遍历
栈和队列
栈
常用pop,push
队列
常用shift,push
链表
创建链表结点
需要一个构造函数:
function ListNode(val) {
this.val = val;
this.next = null;
}
增加节点
// 如果目标结点本来不存在,那么记得手动创建
const node3 = new ListNode(3)
// 把node3的 next 指针指向 node2(即 node1.next) 先后面那条线
node3.next = node1.next
// 把node1的 next 指针指向 node3
node1.next = node3
节点删除
node1.next = node3.next
在涉及链表删除操作的题目中,重点不是定位目标结点,而是定位目标结点的前驱结点。做题时,完全可以只使用一个指针(引用),这个指针用来定位目标结点的前驱结点。
// 利用 node1 可以定位到 node3
const target = node1.next
node1.next = target.next
复杂的都
常规来说,数组都对应着一段连续的内存,删除增加对应的时间复杂度是 O(n)。但是js中,若数组中定义了不同类型的元素
const arr = ['haha', 1, {a:1}]
它对应的就是一段非连续的内存。此时,JS 数组不再具有数组的特征,其底层使用哈希映射分配内存空间,是由对象链表来实现的。
链表增删操作的复杂度是常数级别的复杂度,为 O(1)。 但链表在读取某个特定节点时,必须整个遍历
小结:链表的插入/删除效率较高,而访问效率较低;数组的访问效率较高,而插入效率较低。
树和二叉树
二叉树定义和传值
在 JS 中,二叉树使用对象来定义。它的结构分为三块:
- 数据域
- 左侧子结点(左子树根结点)的引用
- 右侧子结点(右子树根结点)的引用
定义二叉树构造函数时,我们需要把左侧子结点和右侧子结点都预置为空:
// 二叉树结点的构造函数
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
当需要新建一个二叉树结点时,直接调用构造函数、传入数据域的值就行了:
const node = new TreeNode(1)
二叉树遍历
按照实现方式的不同,遍历方式可以分为以下两种:
- 递归遍历(先、中、后序遍历) 以根节点遍历时机为准 常考
- 根结点 -> 左子树 -> 右子树 先
- 左子树 -> 根结点 -> 右子树 中
- 左子树 -> 右子树 -> 根结点 后
- 迭代遍历(层次遍历)
创建
const root = {
val: "A",
left: {
val: "B",
left: {
val: "D"
},
right: {
val: "E"
}
},
right: {
val: "C",
right: "F"
}
}
遍历-递归
编写一个递归函数之前,大家首先要明确两样东西:
- 递归式
- 递归边界
先序
// 所有遍历函数的入参都是树的根结点对象
function preorder(root) {
// 递归边界,root 为空
if(!root) {
return
}
// 输出当前遍历的结点值
console.log('当前遍历的结点值是:', root.val)
// 递归遍历左子树
preorder(root.left)
// 递归遍历右子树
preorder(root.right)
}
中序
// 所有遍历函数的入参都是树的根结点对象
function inorder(root) {
// 递归边界,root 为空
if(!root) {
return
}
// 递归遍历左子树
inorder(root.left)
// 输出当前遍历的结点值
console.log('当前遍历的结点值是:', root.val)
// 递归遍历右子树
inorder(root.right)
}
在leedcode中会要求输出一个数组,这里注意要在里面嵌套一个递归函数
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function (root) {
const res = []
function xunhuan(n){
if (!n) return
xunhuan(n.left)
res.push(n.val)
xunhuan(n.right)
}
xunhuan(root)
return res
};
后序
function postorder(root) {
// 递归边界,root 为空
if(!root) {
return
}
// 递归遍历左子树
postorder(root.left)
// 递归遍历右子树
postorder(root.right)
// 输出当前遍历的结点值
console.log('当前遍历的结点值是:', root.val)
}
时间复杂度
反映的不是算法的逻辑代码到底被执行了多少次,而是随着输入规模的增大,算法对应的执行总次数的一个变化趋势。
只保留次数最高那一项,并且将其常数系数无脑改为1。
举例:
function fn(arr) {
var len = arr.length
for(var i=1;i<len;i=i*2) {
console.log(arr[i])
}
}
这个算法中,我们关心的就是 console.log(arr[i])
到底被执行了几次,换句话说,也就是要知道 i<n
( len === n) 这个条件是在 i
递增多少次后才不成立的。
假设 i
在以 i=i*2
的规则递增了 x
次之后,i<n
开始不成立(反过来说也就是 i>=n
成立)。那么此时我们要计算的其实就是这样一个数学方程:
2^x >= n
x
解出来,就是要大于等于以 2 为底数的 n
的对数:
也就是说,只有当 x
小于 log2n
的时候,循环才是成立的、循环体才能执行。注意涉及到对数的时间复杂度,底数和系数都是要被简化掉的。那么这里的 O(n) 就可以表示为:
O(n) = logn
常见的时间复杂度按照从小到大的顺序排列,有以下几种:
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,是内存增长的趋势。
常见的空间复杂度有 O(1)
、O(n)
和 O(n^2)
。