-
一种分层数据的抽象模型
-
前端工作中常见的树包括:DOM树、级联选择、树形控件…
-
JS 中没有树,但是可以用 Object 和 Array 构建树
-
树的常用操作:深度/广度优先遍历、先中后序遍历
1、普通树
const tree = {
val: "a",
children: [
{
val: "b",
children: [
{
val: "d",
children: [],
},
{
val: "e",
children: [],
},
],
},
{
val: "c",
children: [
{
val: "f",
children: [],
},
{
val: "g",
children: [],
},
],
},
],
}
深度优先遍历
- 深度优先遍历:尽可能深的搜索树的分支,优先遍历其子节点,而非兄弟节点
- 口诀:
-
- 访问根节点
- 对根节点的 children 挨个进行深度优先遍历
const dfs = (root) => {
console.log("root value is", root.val);
root.children.forEach(dfs);
}
dfs(tree);
广度优先遍历
- 广度优先遍历:先访问离根节点最近的节点,优先遍历兄弟节点,再去遍历自己的子节点
- 口诀:
-
- 新建一个队列 并把根节点入队
- 把队头出队并访问
- 把队头的 children 挨个入队
- 重复第二 、三步 直到队列为空
const bfs = (root) => {
const q = [root];
while(q.length > 0) {
const n = q.shift();
console.log(n.val);
n.children.forEach(child => {
q.push(child);
})
}
}
bfs(tree);
2、二叉树
- 树中每个节点最多只能有两个子节点
- 在JS 中通常用 Object 来模拟二叉树
const root = {
val: "A",
left: {
val: "B",
left: {
val: "D",
left: {
val: "DD"
},
right: {
val: "DDD"
}
},
right: {
val: "E",
},
},
right: {
val: "C",
left: {
val: "F",
},
right: {
val: "G",
},
},
}
先序遍历
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
// 先序遍历 递归
const preOrder1 = (tree) => {
// 递归边界,root 为空
if(!tree) return;
// 输出当前遍历的结点值
console.log(tree.val);
// 递归遍历左子树
preOrder1(tree.left);
// 递归遍历右子树
preOrder1(tree.right);
}
// 先序遍历 非递归
const preOrder2 = (tree) => {
if (!tree) return;
// 新建一个栈
const stack = [tree];
while(stack.length) {
const n = stack.pop();
console.log(n.val);
if (n.right) stack.push(n.right);
if (n.left) stack.push(n.left);
}
}
输出结果顺序为:
A
B
D
DD
DDD
E
C
F
G
中序遍历
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
// 中序遍历 递归
const inOrder1 = (tree) => {
// 递归边界,root 为空
if(!tree) return;
// 递归遍历左子树
inOrder1(tree.left)
// 输出当前遍历的结点值
console.log(tree.val)
// 递归遍历右子树
inOrder1(tree.right)
}
// 中序遍历 非递归
const inOrder2 = (tree) => {
if (!tree) return;
// 新建一个栈
const stack = [];
// 先遍历所有的左节点
let p = tree;
while(stack.length || p) {
while(p) {
stack.push(p);
p = p.left;
}
const n = stack.pop();
console.log(n.val);
p = n.right;
}
}
输出结果顺序为:
DD
D
DDD
B
E
A
F
C
G
后序遍历
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点
// 后序遍历 递归
const postOrder1 = (tree) => {
// 递归边界,root 为空
if(!tree) return;
// 递归遍历左子树
postOrder1(tree.left);
// 递归遍历右子树
postOrder1(tree.right);
// 输出当前遍历的结点值
console.log(tree.val);
}
// 后序遍历 非递归
const postOrder2 = (tree) => {
if (!tree) return
const stack = [tree];
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);
}
};
输出结果顺序为:
DD
DDD
D
E
B
F
G
C
A
二叉树的最大深度
// 给一个二叉树,需要你找出其最大的深度,从根节点到叶子节点的距离
// 时间复杂度 O(n) n为树的节点数
// 空间复杂度 有一个递归调用的栈 所以为 O(n) n也是为二叉树的最大深度
const maxDepth = function(root) {
let res = 0;
// 使用深度优先遍历
const dfs = (root, layer) => {
if(!root) return;
if(!root.left && !root.right) {
// 没有叶子节点就把深度数量更新
res = Math.max(res, layer);
}
dfs(root.left, layer + 1);
dfs(root.right, layer + 1);
}
dfs(root, 1);
return res;
};
二叉树的最小深度
解题思路:
- 求最小深度,考虑使用广度优先遍历
- 在广度优先遍历过程中,遇到叶子节点,停止遍历,返回节点层级
解题步骤
- 广度优先遍历整棵树,并记录每个节点的层级
- 遇到叶子节点,返回节点层级,停止遍历
// 时间复杂度O(n),n为树的节点数量
// 空间复杂度O(n),n为树的节点数量
const minDepth = function(root) {
if(!root) return 0;
// 使用广度优先遍历
const q = [[root, 1]];
while(q.length) {
const [n, layer] = q.shift();
// 如果是叶子节点直接返回深度
if(!n.left && !n.right) {
return layer;
}
if(n.left) q.push([n.left, layer + 1]);
if(n.right) q.push([n.right, layer + 1]);
}
};
LeetCode: 102. 二叉树的层序遍历
输出:[[3],[9,20],[15,7]]
解题思路:
- 层序遍历顺序就是广度优先遍历
- 不过在遍历时候需要记录当前节点所处的层级,方便将其添加到不同的数组中
解题步骤:
- 广度优先遍历二叉树
- 遍历过程中,记录每个节点的层级,并将其添加到不同的数组中
const levelOrder = function(root) {
if(!root) return [];
const q = [[root, 0]];
const res = [];
while(q.length) {
const [n, layer] = q.shift();
if(!res[layer]) {
res.push([n.val]);
}else {
res[layer].push(n.val);
}
if(n.left) q.push([n.left, layer + 1]);
if(n.right) q.push([n.right, layer + 1]);
}
return res;
};
另外一种解法:
const 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;
};
上面两个解法
-
时间复杂度O(n),n为树的节点数量
-
空间复杂度O(n),n为树的节点数量
LeetCode: 94. 二叉树的中序遍历
- 将我们上面中序遍历的打印变成往数组里添加元素即可
// 递归版
const inorderTraversal = function(root) {
const res = [];
const rec = (root) => {
if(!root) return;
rec(root.left);
res.push(root.val);
rec(root.right);
}
rec(root);
return res;
};
// 非递归版
const inorderTraversal = function(root) {
const res = [];
const stack = [];
let p = root;
while(stack.length || p) {
while(p) {
stack.push(p);
p = p.left;
}
const n = stack.pop();
res.push(n.val);
p = n.right;
}
return res;
};
LeetCode: 112. 路径总和
解题思路:
- 在深度优先遍历的过程中,记录当前路径的节点值的和
- 在叶子节点处,判断当前路径的节点值的和是否等于目标值
解题步骤:
- 深度优先遍历二叉树,在叶子节点处,判断当前路径的节点值的和是否等于目标值,是就返回 true
- 遍历结束,如果没有匹配,就返回 false
// 时间复杂度O(n),n为树的节点数量
// 空间复杂度O(n),n递归堆栈的高度,也就是树的高度,树的高度最小一条分叉的时候为n,如果全部都有两条分叉则为logn
const hasPathSum = function (root, targetSum) {
if (!root) return false;
let res = false;
const dfs = (n, sum) => {
// 在叶子节点判断是否有相等值
if (!n.left && !n.right && targetSum === sum) {
res = true;
}
// 记录每个节点路径的和
if (n.left) dfs(n.left, sum + n.left.val);
if (n.right) dfs(n.right, sum + n.right.val);
};
dfs(root, root.val);
return res;
}
遍历JSON的所有节点值
const obj = { a: { b: { c: 3 } }, d: 4 };
const bfs = (n, path) => {
console.log("path is", path, "n is", n);
Object.keys(n).forEach((key) => {
bfs(n[key], path.concat(key));
});
}
bfs(obj, []);
打印结果:
path is [] n is {a: {…}, d: 4}
path is ['a'] n is b: {c: 3}
path is ['a', 'b'] n is {c: 3}
path is ['a', 'b', 'c'] n is 3
path is ['d'] n is 4
渲染Antd中的树组件
const json = [
{ title: "一", key: "1", children: [{ title: "三", key: "3" }] },
{ title: "二", key: "2", children: [{ title: "四", key: "4" }] },
];
class Demo extends React.component {
dfs = (n) => {
return (
<TreeNode title={n.title} key={n.key}>
{n.children.map(this.dfs)}
</TreeNode>
);
};
render() {
return <Tree>{json.map(this.dfs)}</Tree>;
}
}