树
- 一种分层数据的抽象模型
- 前端工作中常见的树包括: DOM树、级联选择(省市区)、树形控件
- js中没有树,但可以用Object和Array构建树
- 常用操作:深度/广度优先遍历、先中后序遍历
深度优先遍历:尽可能深的搜索树的分支
广度优先遍历:先访问离根节点最近的节点,数字即遍历顺序
深度优先遍历算法
- 访问根节点
- 对根节点children挨个进行深度优先遍历
const tree = {
val: 'a',
children: [{
val: 'b',
children: [
{val: 'd', children: []},
{val: 'e', children: []}
]
},{
val: 'c', children: [
{val: 'f', children:[]},
{val: 'g', children:[]}
]
}]
}
const dfs = (root) = {
console.log(root.val) // a b d e c f g
root.children.forEach(dfs)
}
广度优先遍历算法
- 新建一个队列,把根节点入队
- 把对头出对并访问
- 把对头的children挨个入队
- 重复第二、三步,直到队列为空
const bfs = (root) => {
const q = [root]
while(q.length > 0) {
const n = q.shift()
console.log(n.val) // a b c d e f g
n.children.forEach(child => {
q.push(child)
})
}
}
闻风丧胆的二叉树
- 树中每个节点最多有两个字节点
- 在js中通常用Object来模拟二叉树
// 二叉树代码写法
const binaryTree = {
val: 1,
left: {
val: 2,
left: null,
right: null
},
right: {
val: 3,
left: null,
right: null
}
}
二叉树先序遍历
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
const binaryTree = {
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: null,
right: null
},
right: {
val: 7,
left: null,
right: null
}
}
}
// 递归版
const preorder = (root) => {
if (!root) return;
console.log(root.val) // 1 2 4 5 3 6 7
preorder(root.left)
preorder(root.right)
}
preorder(binaryTree)
// 非递归版 利用栈的后进先出
const preorder = (root) => {
if (!root) return;
const stack = [root]
while(stack.length) {
const n = stack.pop()
console.log(n.val) // 1 2 4 5 3 6 7
if (n.right) stack.push(n.right)
if (n.left) stack.push(n.left)
}
}
二叉树中序遍历
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
// 递归版
const inorder = (root) => {
if (!root) return;
inorder(root.left)
console.log(root.val) // 4 2 5 1 6 3 7
inorder(root.right)
}
inorder(binaryTree)
// 非递归版
const preorder = (root) => {
if (!root) return;
const stack = []
let p = root
while(stack.length || p) {
while(p) {
stack.push(p)
p = p.left
}
const n = stack.pop()
console.log(n.val) // 4 2 5 1 6 3 7
p = n.right
}
}
二叉树后序遍历
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点
// 递归版
const postorder = (root) => {
if (!root) return;
postorder(root.left)
postorder(root.right)
console.log(root.val) // 4 5 2 6 7 3 1
}
postorder(binaryTree)
// 非递归版
const postorder = (root) => {
if (!root) return
const stack = [root]
const outputstack = []
while (stack.length) {
const n = stack.pop()
outputstack.push(n)
if (n.left) stack.push(n.left)
if (n.right) stack.push(n.right)
}
while (outputstack.length) {
const n = outputstack.pop()
console.log(n.val)
}
}
leetcode104:二叉树最大深度
- 解题思路
- 求最大深度,考虑使用深度优先遍历
- 深度优先遍历过程中,记录每个节点所在的层级,找出最大的层级即可
- 解题步骤
- 新建一个变量,记录最大深度
- 深度优先遍历整棵树,并记录每个节点的层级,同时不断刷新最大深度这个变量
- 遍历结束返回最大深度变量
- code
// 时间复杂度O(n)
// 最差空间复杂度O(n) 最好的情况O(log n) 虽然代码中没有定义变量,但是递归存在调用栈,所以最差的情况即有多少个节点就有多少层,调用栈即存在多少,即O(n)
var maxDep = function(root) {
let res = 0
const dfs = (n, level) => {
if(!n) return
if (!n.left && !n.right) res = Math.max(res, level)
console.log(n.val)
dfs(n.left, level + 1);
dfs(n.right, level + 1)
}
dfs(root, 1)
return res
leetcode111:二叉树最小深度
- 解题思路
- 求最小深度,虽然可以使用深度优先遍历,但考虑使用广度优先遍历
- 广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级,此时不需要再次遍历其他叶子节点,比深度优先遍历快
- 解题步骤
- 广度优先遍历整棵树,并记录每个节点的层级
- 遇到叶子节点,返回节点层级,停止遍历
- code
// 时间复杂度O(n) 空间复杂度O(n)
var minDep = function(root){
if (!root) return 0
const q = [[root, 1]]
while(q.length) {
const [n, level] = q.shift()
console.log(n.val)
if (!n.left && !n.right) {return level}
if (n.left) q.push([n.left, level + 1])
if (n.right) q.push([n.right, level + 1])
}
}
leetcode102:二叉树的层序遍历
- 解题思路
- 层序遍历顺序就是广度优先遍历
- 不过在遍历时候需要记录当前节点的层级,方便将其添加在所在数组中
- 解题步骤
- 广度优先遍历整棵树
- 遍历时,记录每个节点的层级,并将其添加到不同的数组中
- code
// 方法一
var levelOrder = function(root){
if (!root) return []
const q = [[root, 0]]
const res = []
while(q.length){
const [n, level] = q.shift()
if (!res[level]) {
res.push([n.val])
} else {
res[level].push(n.val)
}
if (n.left) q.push([n.left, level + 1])
if (n.right) q.push([n.right, level + 1])
}
}
// 方法二
// 统一层级的老成员先处栈,新的同一层级成员入栈
var levelOrder = function(root){
if (!root) return []
const q = [root]
const res = []
while(q.length){
let len = q.length
res.push([])
while(len--) {
const n = q.shift()
res[res.length - 1].push(n.val)
if (n.left) q.push(n.left)
if (n.right) q.push(n.right)
}
}
return res
}
leetcode112:路径总和
- 解题思路
- 深度优先遍历的过程中,记录当前路径的节点值的和
- 在叶子节点处,判断当前路径的节点和是否等于目标值
- code
// 时间复杂度O(n) 空间复杂度O(n)
var hasPathSum = function(root, sum) {
if (!root) return false
let res = false
const dfs = (n, s) => {
if (!n.left && !n.right && s === sum) res = true
if (n.left) dfs(n.left, s + n.left.val)
if (n.right) dfs(n.right, s + n.right.val)
}
dfs(root, root.val)
return res
}