字典 Map
- 与集合类似,但它是以
键值对的形式存储的。 - ES6中有字典,名为
Map
// 增 set
// 删除 delete
// 改(覆盖) set
// 清空 clear
// 交集 可用用过字典来完成数组的交集,思想就是先将A数组迭代到字典里面,然后在迭代B数组,每层循环的值判断是否存在于字典里面,存在则push到交集数组里面
总结
- 与集合类似,字典也是一种存储唯一值得数据结构,但它是以键值对的形式来存储的
- ES6中有字典,名为Map
- 算法用到字典的有
两个数组的交集,无重复字符的最长子串,两数之和
树 Three
- 一种分层数据的抽象模型
- 前端工作中常见的树包括:DOM树、级联选择(城市区选择 slect)、树形控件
- JS中没有树,但是我们可用Object和Array构建树
- 树的常用操作:
深度/广度优先遍历、先中后序遍历(二叉树)。
深度优先遍历:就像我们平常看书一样,先看第一章,然后是第一章的内容。一页一页看的。
广度优先遍历:就像我们平常看书一样,先把整本书的目录全部看一遍,然后去看第一章的内容。
根据经验之谈,建议大家拿到一本书之后先以广度优先遍历的策略去阅读书。
深度优先遍历算法口诀(递归)
- 访问根节点
- 对根节点的children依次进行深度优先遍历
背了【访问根节点、对根节点的children依次进行深度优先遍历】走到那里都不怕深度优先遍历
let tree = {
val: 'a',
children: [
{
val: 'b',
children: [
{
val: 'd',
children: []
},
{
val: 'e',
children: []
}
]
},
{
val: 'c',
children: [
{
val: 'f',
children: []
},
{
val: 'g',
children: []
}
]
}
]
}
// 树的深度优先遍历
function dfs(tree) {
// 访问根节点
console.log(tree.val)
// 依次递归
if (tree.children.length > 0) {
tree.children.forEach((child) => {
dfs(child)
})
}
}
dfs(tree)
// a
// b
// d
// e
// c
// f
// g
广度优先遍历算法口诀(队列)
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的children挨个入队
- 重复第二、三步,直到队列为空
实现广度优先遍历,就要用到 队列 、while 、 shift()
let tree = {
val: 'a',
children: [
{
val: 'b',
children: [
{
val: 'd',
children: []
},
{
val: 'e',
children: []
}
]
},
{
val: 'c',
children: [
{
val: 'f',
children: []
},
{
val: 'g',
children: []
}
]
}
]
}
// 广度优先遍历
function bfs(tree) {
let queue = [tree]
while (queue.length > 0) {
const n = queue.shift()
console.log(n.val)
n.children.forEach((child) => {
queue.push(child)
})
}
}
bfs(tree)
// a
// b
// c
// d
// e
// f
// g
二叉树的先中后序遍历(递归版本)
- 树中每个节点最多只能有两个子节点
- 树的前序遍历, 中序遍历,后序遍历,都属于深度优先遍历。
- 在JS中通常用Object来模拟二叉树
-
先序遍历算法口诀(每个根节点开始都是一个递归)
1.访问根节点
2.对根节点的左子树进行先序遍历
3.对根节点的右子树进行现需遍历
function preorder(bt) {
// 访问根节点
// 对根节点的左子树进行先序遍历
// 对根节点的右子树进行先序遍历
console.log(bt.val)
if (bt.left) {
preorder(bt.left)
}
if (bt.right) {
preorder(bt.right)
}
}
中序遍历算法口诀(左根右)
function midorder(bt) {
if (bt.left) {
midorder(bt.left)
}
console.log(bt.val)
if (bt.right) {
midorder(bt.right)
}
}
后序遍历算法口诀(左右根)
function postorder(bt) {
if (bt.left) {
postorder(bt.left)
}
if (bt.right) {
postorder(bt.right)
}
console.log(bt.val)
}
二叉树
const bt = {
val: 1,
left: {
val: 2,
left: {
val: 4,
left: null,
right: null
},
right: {
val: 5,
left: null,
right: null
}
},
right: {
val: 3,
left: {
val: 6,
left: {
left: null,
right: null
}
},
right: {
val: 7,
left: null,
right: null
}
}
}
二叉树的先中后序遍历(非递归版本)
- 在面试中,递归当时的前中后序遍历太简单了,面试官会筛选高水平的选手要求
非递归的算法去实现前中后序遍历 - 非递归的前中后序遍历是用的
函数调用栈的思想,要用到栈 倒着写push因为是栈,后进先出pop while 比如 ”左右根“,
// 先序遍历二叉树
// 根左右
// 利用函数调用栈思想
function preorder(tree) {
let stack = []
stack.push(tree)
while (stack.length > 0) {
const node = stack.pop()
console.log(node.val)
if (node.right) {
stack.push(node.right)
}
if (node.left) {
stack.push(node.left)
}
}
}
// 后序遍历二叉树 [左右根],利用函数调用栈思想
// 其实后序遍历就是前序遍历,利用前序遍历二叉树的算法,前序是【根左右】, 后序是【左右根】,那么我们把后序遍历倒置一下就是【根右左】
// 【根右左】就很像我们的前序遍历 【根左右】,只不过在访问根的时候我们给记录下来outStack,然后依次pop就是我们要的答案
// 我们通过前序的算法,稍作改动
function postorder(tree) {
let stack = []
let outStack = []
stack.push(tree)
while (stack.length > 0) {
const node = stack.pop()
// 1、根
outStack.push(node.val)
// 3、左
if (node.left) {
stack.push(node.left)
}
// 2、右
if (node.right) {
stack.push(node.right)
}
// 我们的顺序是 1 3 2 原因是我们利用栈的特性,后进先出
}
while (outStack.length > 0) {
console.log(outStack.pop())
}
}
中序遍历(非递归版本)
// 中序遍历二叉树 [左根右]
// 利用指针 p
// 先把左子树全部push到栈,然后输出,然后指针改成右子树
function inorder(tree) {
let stack = []
let p = tree
while (stack.length > 0 || p) {
while (p) {
stack.push(p)
p = p.left
}
const n = stack.pop()
console.log(n.val)
p = n.right
}
}
inorder(bt)
遍历JSON的所有节点值
实际运用场景:处理后端返回的json数据
渲染Antd中的树组件
总结
二叉树前中序遍历分为2个版本,一个是递归版本比较简单,按照 “根左右” ”左根右“ ”左右根“ 依次进行递归就可以,第二个版本是利用函数堆栈的思想,就是利用栈的概念,前序与后序其实很类似,向stack内push,只不过要注意的是后序遍历其实就是用的前序遍历的思想,唯独比较难的是中序遍历(非递归版本),因为要用到指针。
树是一种分层数据的抽象模型,在前端广泛应用
编码
349.两个数组的交集 LeetCode
时间复杂度 O(n^2) filter * has
空间复杂度 O(n)
总结:需要用到集合的has方法与数组的filter方法
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
* 交集-就要使用Set集合
*/
var intersection = function(nums1, nums2) {
// [1, 2, 2, 1] [2, 2]
let set = new Set(nums1)
return [... new Set( nums2.filter((item) => set.has(item)) )]
};
通过字典Map的方式来实现两个数组的交集
时间复杂度 O(n)
空间复杂度 O(n)
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function(nums1, nums2) {
// 通过字典来实现
let map = new Map()
nums1.forEach((item) => { // 空间复杂度 O(n)
map.set(item, true)
})
let res = []
nums2.forEach((item) => {
if(map.get(item)) {
res.push(item)
map.delete(item)
}
})
return res
};
3.无重复字符的最长子串 LeetCode
解题步骤
双指针维护一个滑动窗口(就好像切换音频)- 不断的移动右指针,遇到重复的字符,就把左指针移动到重复字符的下一位
- 过程中,记录所有窗口的长度,并返回最大值
解题知识点:双指针、滑动窗口、字典
边界:获取的重复字符必须在滑动窗口内 map.get(s[r]) ≥ l)
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
// 双指针、滑动窗口、字典、边界(map.get(s[r]) >= l)
let l = 0;
// "abcabcbb"
// l
// r
let res = 0;
let map = new Map()
// 右指针向右移动
for(let r = 0; r < s.length; r++) {
if(map.has(s[r]) && map.get(s[r]) >= l) {
l = map.get(s[r]) + 1
}
res = Math.max(res, r - l + 1) // 1 2 3
map.set(s[r], r) // a:0 b:1 c:2
}
return res
};
// 边界
// "abba"
// 第一次
// r l res map
// 0 0 1 a:0
// 1 2 2 a:0 b:1
// 2 2 1 a:0 b:1 b:2
// 3 如果没有 map.get(s[r]) >= l 限制,就会取滑动窗口之外取值
76.最小覆盖子串 LeetCode
104.二叉树的最大深度 LeetCode
时间复杂度 O(n)
空间复杂度O(nlogn)最好的情况,最坏的情况是O(n) //它的空间复杂度看似是O(1)实则不是,因为在函数内调用函数会形成函数调用堆栈,我们这里的dfs深度优先遍历也不例外,它也形成了堆栈,这样的空间复杂度我们值需要嵌套了多少层就可以,我们dfs嵌套的层数就是二叉树的最大深度
dfs Math.max
/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {number}
*/
var maxDepth = function(root) {
// dfs
let res = 0;
function dfs(n, l) {
if(!n){return false}
res = Math.max(res, l)
if(n.left) {
dfs(n.left, l+1)
}
if(n.right) {
dfs(n.right, l+1)
}
}
dfs(root, 1)
return res
};
// 优化版本,只有在叶子结点的时候才进行最深层数的比较
var maxDepth = function(root) {
// dfs
let res = 0;
function dfs(n, l) {
if(!n){return false}
// 优化,只有在叶子结点的时候才计算
if(!n.left && !n.right) {
res = Math.max(res, l)
}
if(n.left) {
dfs(n.left, l+1)
}
if(n.right) {
dfs(n.right, l+1)
}
}
dfs(root, 1)
return res
}
101.二叉树的最小深度 LeetCode
最佳算法:广度优先遍历
/**
* 第一次做这道题的时候,首先参考的是二叉树最大深度的思路,直接将max改成min,但是不行,因为每一次dfs都会 l从小到大 如果直接比较min,不准确
* 最小深度的时机应该是没有左右子树的时候,因此这道题的关键点是 if(!root.left && !root.right) minArr.push(l)
*/
var minDepth = function(root) {
if(!root) {
return 0
}
let minArr = [];
// dfs
function dfs(root, l) {
// 访问跟节点
console.log(root.val)
if(!root.left && !root.right) {
minArr.push(l)
}
if(root.left) {
dfs(root.left, l + 1)
}
if(root.right) {
dfs(root.right, l + 1)
}
}
dfs(root, 1)
return Math.min(...minArr)
}
// 这道题是求最小深度,那么我们最佳的使用算法是应该使用广度优先遍历
// 第一次尝试做的时候bfs遍历还是很顺利的,但是在实现【记录每个节点的层级】卡壳了 那么我们该怎么做?
// 很神奇的一种操作,我们在通过递归的时候是通过 dfs(root.left, l + 1) 传递了 l + 1作为了层次,如果在队列过程中模拟该方法的实现?
// 点睛之笔: let queue = [[root]] 改写为 let queue = [[root, 1], [root, l+1]] 记录一下层级
// 时间复杂度O(n) 空间复杂度O(n)
var minDepth = function(root) {
// 广度优先遍历
if(!root) {
return 0
}
let queue = [[root, 1]] // 点睛之笔,正常我们bfs广度遍历的时候只是 let queue = [root]
while(queue.length > 0) {
let [n, l] = queue.shift()
if(!n.left && !n.right) {
return l
}
if(n.left) {
queue.push([n.left, l + 1])
}
if(n.right) {
queue.push([n.right, l + 1])
}
}
}
102.二叉树的层序遍历 LeetCode
当时在做这道题的时候遇见一个逻辑实现问题:依次输出的 3,0 9,1 20,1 15,2 7,2 怎么转换为 [[3],[9,20],[15,7]]
/**
* 队列 bfs 【记录每层-点睛之笔 queue = [[root, 1]]】
*/
var levelOrder = function(root) {
// bfs queue = [root, l]
if(!root) {
return []
}
let arr = []
let map = new Map()
let queue = [[root, 1]] // 点睛之笔
while(queue.length > 0) {
let [n, l] = queue.shift();
// 依次输出的 3,0 9,1 20,1 15,2 7,2 怎么转换为 [[3],[9,20],[15,7]]
if(!arr[l-1]) {
arr.push([n.val])
} else {
arr[l-1].push(n.val)
}
n.left && queue.push([n.left, l + 1])
n.right && queue.push([n.right,l + 1])
}
return arr
};
112.路径总和 LeetCode
/**
* @param {TreeNode} root
* @param {number} targetSum
* @return {boolean}
* 这道题与二叉树的层序遍历比较像,那个是要记录每一层,这个要记录每个节点相加值
*/
var hasPathSum = function(root, targetSum) {
// dfs遍历每一个节点时候附带当前节点与之前路径节点的和
// 时间复杂度O(n) 空间复杂度O(n)
if(!root) {
return false
}
let flag = false
// 第一步 实现dfs
function dfs(n, s) {
console.log(n.val, s)
if(s === targetSum && !n.left && !n.right) {
flag = true
}
n.left && dfs(n.left, s + n.left.val)
n.right && dfs(n.right, s + n.right.val)
}
dfs(root, root.val)
return flag
};