树 概述
树是一种常用的数据结构,用来模拟具有树状结构的数据集合。
树里的每一个节点有一个值和一个包含所有子节点的列表(又是一个树,称为子树)。
从图的观点来看,树可以看成是一个拥有 N个节点 和 N - 1条边 的一个有向无环图。
二叉树是一种更为典型的树形结构,顾名思义,二叉树就是每个节点最多有两个子树的树结构,通常称为左子树和右子树
树的深度优先遍历 (DFS)
遍历之前要先了解 树 的构造函数
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
二叉树示例图:
以上述中,前中后序遍历顺序如下:
- 前序遍历(根左右):5 4 1 2 6 7 8
- 中序遍历(左根右):1 4 2 5 7 6 8
- 后序遍历(左右根):1 2 4 7 8 6 5
前序遍历
前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树 根左右
递归算法: 时间复杂度O(n) 空间复杂度O(n)
/*
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
if (!root) return []
return [root.val].concat(preorderTraversal(root.left), preorderTraversal(root.right))
};
迭代算法: 时间复杂度O(n) 空间复杂度O(n)
/**
* @param {TreeNode} root
* @return {number[]}
*/
var preorderTraversal = function(root) {
if (!root) return []
const stack = []
const res = []
stack.push(root)
while (stack.length) {
const node = stack.pop()
res.push(node.val)
if (node.right) stack.push(node.right)
if (node.left) stack.push(node.left)
}
return res
};
中序遍历
中序遍历是先遍历左子树,然后访问根节点,然后遍历右子树 左根右
递归算法: 时间复杂度O(n) 空间复杂度O(n)
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
if (!root) return []
return inorderTraversal(root.left).concat(root.val, inorderTraversal(root.right))
};
迭代算法: 时间复杂度O(n) 空间复杂度O(n)
/**
* @param {TreeNode} root
* @return {number[]}
*/
var inorderTraversal = function(root) {
if (!root) return []
const result = [], stack = []
while (root !== null || stack.length > 0) {
while(root) {
stack.push(root)
root = root.left
}
const pop = stack.pop()
result.push(pop.val)
root = pop.right
}
return result
};
后序遍历
后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根结点 左右根
递归算法: 时间复杂度O(n) 空间复杂度O(n)
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
if (!root) return []
return postorderTraversal(root.left).concat(postorderTraversal(root.right), root.val)
}
迭代算法: 时间复杂度O(n) 空间复杂度O(n)
/**
* @param {TreeNode} root
* @return {number[]}
*/
var postorderTraversal = function(root) {
if (!root) return []
let stack = []
let res = []
let prev = null
while (root != null || stack.length) {
while (root != null) {
stack.push(root)
root = root.left
}
root = stack.pop()
if (root.right == null || root.right == prev) {
res.push(root.val)
prev = root
root = null
} else {
stack.push(root)
root = root.right
}
}
return res
};
Morris 遍历: 时间复杂度O(n) 空间复杂度O(1)
// morris 遍历
var postorderTraversal = function(root) {
let res = []
if (!root) return res
const addPath = (res, node) => {
let tmp = []
while (node != null) {
tmp.push(node.val)
node = node .right
}
for (let i = tmp.length - 1; i >= 0; i--) {
res.push(tmp[i])
}
}
let p1 = root, p2 = null
while (p1 != null) {
p2 = p1.left
if (p2 != null) {
while (p2.right != null && p2.right != p1) {
p2 = p2.right
}
if (p2.right == null) {
p2.right = p1
p1 = p1.left
continue
} else {
p2.right = null
addPath(res, p1.left)
}
}
p1 = p1.right
}
addPath(res, root)
return res
};
注意点
- 删除树的某个节点的时候,删除过程以后序遍历的顺序进行。就是说,当你删除一个节点的时候,会先删除它的左子节点和右子节点,然后才是删除本身。
技巧
- 后序遍历的时候使用栈来处理表达式会更简单:
每遇到一个操作符,就从栈中弹出栈顶两个元素,计算并返回结果
树的广度优先遍历 (BFS)
广度优先搜索是一种广泛运用在树或者图这类数据结构中,遍历或搜索的算法。
该算法从一个根节点开始,首先访问节点本身;然后遍历它的相邻节点,其次遍历它的二级邻节点、三级邻节点,以此类推。
层序遍历
二叉树示例图:
以上述中,层序遍历顺序如下:
- 层序遍历: 5 46 1278
层序遍历迭代算法:
var levelOrder = function(root) {
let res = []
if (!root) return res
let queue = [root] // 队列拿到root节点
while (queue.length) {
let currentLength = queue.length
res.push([]) // 每一层是一个数组
for (let i = 0; i < currentLength; i++) {
let node = queue.shift() // 拿出第一个节点
res[res.length - 1].push(node.val) // 把这个节点的值推入当前这一层的数组中
if (node.left) queue.push(node.left) // 存在左右节点则分别进队列
if (node.right) queue.push(node.right)
}
}
return res
};
运用递归解决树的问题
递归是树的特性之一,利用递归解决单个节点内的问题,递归调用函数来解决其子节点的问题。
通常,我们可以通过自顶向下或者自底向上的递归来解决问题
“自顶向下”的解决方案
“自顶向下”可以被认为是一种前序遍历。
一句话描述“自顶向下”: 上层数值将值传递给下层,直到最后一层停止递归。
递归函数模板
var top_down = function(root, params) {
// 1. 特殊值检查(空值返回)
// 2. 返回值更新(如果需要的话)
// 3. 获取左子节点的值: left_ans = top_down(root.left, params)
// 4. 获取右子节点的值: right_ans = top_down(root.right, params)
// 5. 返回最终值
}
/**
* maximum_depth 函数: 伪代码 求最大深度
* @params root 根节点
* @params depth 深度
*/
var maximum_depth = function(root, depth) {
let answer = 0 // 0. 别忘了定义变量
// 1. 特殊值检查(空值返回) root节点不存在返回 0 深度
if (!root) return 0
// 2. 返回值更新(如果需要的话) 这里假设当前节点均无左右节点,拿到当前更深的深度作为答案
if (root.left == null && root.right == null) {
answer = Math.max(answer, depth)
}
// 3. 获取左子节点的值
maximum_depth(root.left, depth + 1)
// 4. 获取右子节点的值
maximum_depth(root.right, depth + 1)
// 5. 返回最终值 返回更大的值
return answer
}
注意上面的获取深度函数,是从上往下的去迭代更新answer这个值
“自底向上”的解决方案
“自底向上”可以被认为是一种后序遍历。
一句话描述“自底向上”: 上层数值依赖于下层数值,最后得到两个值(左右节点的所需值如深度等)取最大值即可。
递归函数模板
var bottom_up = function(root, params) {
// 1. 特殊值检查(空值返回)
// 2. 返回值更新(如果需要的话)
// 3. 获取左子节点的值: left_ans = bottom_up(root.left, params)
// 4. 获取右子节点的值: right_ans = bottom_up(root.right, params)
// 5. 返回最终值
}
/**
* maximum_depth 函数: 伪代码
* @params root 根节点
*/
var maximum_depth = function(root) {
// 1. 特殊值检查(空值返回) root节点不存在返回 0 深度
if (!root) return 0
// 2. 返回值更新(如果需要的话) 这里就不需要了
// 3. 获取左子节点的值
let left_depth = maximum_depth(root.left)
// 4. 获取右子节点的值
let right_depth = maximum_depth(root.right)
// 5. 返回最终值 注意这里的 + 1 就是让深度 + 1
return Math.max(left_depth, right_depth) + 1
}
什么时候该“自顶向下”?什么时候该“自底向上”?
对于当前题目:
- 你能确定一些参数,从该节点自身解决出发寻找答案吗?
- 你可以使用这些参数和节点本身的值来决定传递给它子节点的参数吗?
如果都可以,那么就可以尝试使用“自顶向下”的递归来解决问题
对于当前题目:
- 如果你知道它子节点的答案,你能计算出该节点的答案吗?
如果可以,那么就可以尝试“自底向上”的递归来解决问题
题目练习
- 前序遍历: 144.二叉树的前序遍历
- 中序遍历: 94. 二叉树的中序遍历
- 后序遍历: 145.二叉树的后序遍历
- 层序遍历: 102. 二叉树的层序遍历
- 二叉树属性: