目录
第八章 树
8.1 树简介
- 树:一种分层数据的抽象类型。
- 前端工作中常见的树:DOM、树、级联选择、树形控件…
- JS中没有树,但是可以用Object和Array构建树
- 树的常用操作:深度/广度优先遍历、先中后序遍历
8.2 深度与广度优先遍历
1.深度优先遍历:先访问一个节点下的深层次的节点(即尽可能深的搜索树的分支)
2.广度优先遍历:先横向遍历同级节点,再遍历下级的同级节点(即先访问离根节点最近的节点)
- 深度优先遍历(其实就是一个递归)
算法思想:
1.访问根节点
2.对根节点的children 挨个进行深度优先遍历
// 用Object声明一颗树,新建一颗树
//var tree = {val:值,child[],...}
const tree = {
val: 'a',//有一个val节点
children: [
{
val: 'b',//有一个val节点
children: [
{
val: 'd',//有一个val节点
children: [],//有一个children数组
},
{
val: 'e',//有一个val节点
children: [],//有一个children数组
}
],//有一个children数组
},
{
val: 'c',//有一个val节点
children: [
{ > val: 'f',//有一个val节点
children: [],//有一个children数组
},
{
val: 'g',//有一个val节点
children: [],//有一个children数组
}
],//有一个children数组
}
],//有一个children数组
}
const dfs = (root) => {// 新建一个函数,让这个函数接收这个root的根节点
console.log(root.val); //访问这个根节点
// root.children.forEach((child) => { //对这个根节点的children挨个进行深度优先遍历
// dfs(child)
// });
root.children.forEach(dfs);//对这个根节点的children挨个进行深度优先遍历
};
dfs(tree); //把整颗树传进去
4. 广度优先遍历:
- 新建一个队列,把根节点入队
- 把队头出队并访问
- 把队头的 children 挨个入队
- 重复第二、第三步,直到队列为空
// 用Object声明一颗树,新建一颗树
const tree = {
val: 'a',//有一个val节点
children: [
{
val: 'b',//有一个val节点
children: [
{
val: 'd',//有一个val节点
children: [],//有一个children数组
},
{
val: 'e',//有一个val节点
children: [],//有一个children数组
}
],//有一个children数组
},
{
val: 'c',//有一个val节点
children: [
{
val: 'f',//有一个val节点
children: [],//有一个children数组
},
{
val: 'g',//有一个val节点
children: [],//有一个children数组
}
],//有一个children数组
}
],//有一个children数组
}
const bfs = (root) => {
const q = [root]; //新建一个队列Q,并把根节点入队
while (q.length > 0) { // 在队列不为空的情况下,不断地循环第二、三步
const n = q.shift();//队头出队
console.log(n.val); //访问这个队头
n.children.forEach(child => { // 把对头的child挨个入队
q.push(child);//把child加入队列中
});// 把队头的child挨个入队
}
}
bfs(tree); //把整颗树传进去
8.3 二叉树的先中后序遍历
- 树的类型:
- 多叉树:树中每个节点有多个子节点
- 二叉树:树中每个节点最多只能有两个子节点
- 二叉树遍历方式
- 先序遍历算法口诀(根左右)
- 访问根节点
- 对根节点的左子树进行先序遍历
- 对根节点的右子树进行先序遍历
- 中序遍历算法口诀(左根右)
- 对根节点的左子树进行中序遍历
- 访问根节点
- 对根节点的右子树进行中序遍历
- 后序遍历算法口诀(左右根)
- 对根节点的左子树进行后序遍历
- 对根节点的右子树进行后序遍历
- 访问根节点
建二叉树存于文件bt.js中
// 用Object声明一颗树,新建一颗树
const tree = {
val: 'a',//有一个val节点
children: [
{
val: 'b',//有一个val节点
children: [
{
val: 'd',//有一个val节点
children: [],//有一个children数组
},
{
val: 'e',//有一个val节点
children: [],//有一个children数组
}
],//有一个children数组
},
{
val: 'c',//有一个val节点
children: [
{
val: 'f',//有一个val节点
children: [],//有一个children数组
},
{
val: 'g',//有一个val节点
children: [],//有一个children数组
}
],//有一个children数组
}
],//有一个children数组
}
const bfs = (root) => {
const q = [root]; //新建一个队列Q,并把根节点入队
while (q.length > 0) { // 在队列不为空的情况下,不断地循环第二、三步
const n = q.shift();//队头出队
console.log(n.val); //访问这个队头
n.children.forEach(child => { // 把对头的child挨个入队
q.push(child);//把child加入队列中
});// 把队头的child挨个入队
}
}
bfs(tree); //把整颗树传进去
先序遍历算法实现(递归版)
const bt = require('./bt'); //利用require语法导入构建二叉树的JS文件
//写一个先序遍历的方法
const preorder = (root) => {
if (!root) {return;}// 如果为空则不要了
console.log(root.val); //访问根节点
preorder(root.left); //递归访问左子树
preorder(root.right); //递归访问右子树
};
preorder(bt); //调用先序遍历的方法,把二叉树丢进去
中序遍历算法实现(递归版)
const bt = require('./bt'); //利用require语法导入二叉树文件
const inorder = (root) => {//写一个中序遍历的方法,参数为root
if (!root) {// 如果为空则不要了
return;
}
preorder(root.left); //递归访问左子树
console.log(root.val); //访问根节点
preorder(root.right); //递归访问右子树
};
inorder(bt); //调用中序遍历的方法,把二叉树丢进去
后序遍历算法实现(递归版)
const bt = require('./bt'); //利用require语法导入二叉树文件
//写一个后序遍历的方法,参数为root
const postorder = (root) => {
if (!root) { // 根节点如果为空则不要了
return;
}
preorder(root.left); //递归访问左子树
preorder(root.right); //递归访问右子树
console.log(root.val); //访问根节点
};
postorder(bt); //调用后序遍历的方法,把二叉树丢进去
8.4 二叉树的先中后序遍历(非递归版)
1. 先序遍历
const bt = require('./bt'); //利用require语法导入二叉树文件
const preorder = (root) => {//写一个先序遍历的方法,参数为root
if (!root) {// 根节点如果为空则不要了
return;
}
const stack = [root];// 新建一个stack来模拟函数调用堆栈。把root传进栈,代表当前访问的这个节点是根节点
// 我们知道如果在函数里面调用另外一个函数,则我们要往栈里面再推入一个函数
// 用一个while循环来
while (stack.length) {//当stack有值的时候跑起来
const n = stack.pop(); //把根节点的值弹出来,下面再访问它
console.log(n.val); //直接访问它
// 如果根节点存在的话,我们就把它推入函数的调用堆栈中
//根据栈的后进先出的特性,right要比left先进入栈
if (n.right) {
stack.push(n.right);//right进栈
}
if (n.left) {
stack.push(n.left);//left进栈
}
}
};
preorder(bt);//调用先序遍历的方法,把二叉树丢进去
2. 中序遍历
const bt = require('./bt'); //利用require语法导入二叉树文件
//写一个中序遍历的方法,参数为root
const inorder = (root) => {
// 根节点如果为空则不要了
if (!root) {
return;
}
// 我们知道如果在函数里面调用另外一个函数,则我们要往栈里面再推入一个函数
const stack = [];// 新建一个stack代表一个函数的调用堆栈。把root传进栈,代表当前访问的这个节点是根节点
// 第一步要把所有的左子树全部入栈,需要用到一个指针
let p = root; //新建一个指针
while (stack.length || p) {
// 遍历整棵树的左节点,并把所有的左节点都推入栈中
while (p) {
stack.push(p); //每遍历一个节点都把它推入栈中
p = p.left;//在指针有值的情况下
}
const n = stack.pop(); // 把最尽头的左节点弹出来
console.log(n.val);//并访问左节点的值
p = n.right; // 访问右节点,直接把指针指向右节点
}
};
inorder(bt);//调用中序遍历的方法,把二叉树丢进去
3. 后序遍历
把先序遍历倒过来就是后序遍历了,但倒过来左右孩子还是不一样,需要改写一下
const bt = require('./bt'); //利用require语法导入二叉树文件
//写一个后序遍历的方法,参数为root
const postorder = (root) => {
if (!root) {// 根节点如果为空则不要了
return;
}
// 需要用到两个栈,声明两个栈
const stack = [root];
const outputStack = []; //这个栈用来倒置
// 我们知道如果在函数里面调用另外一个函数,则我们要往栈里面再推入一个函数
// 用一个while循环来
while (stack.length) {
const n = stack.pop(); //把根节点的值弹出来,下面再访问它
outputStack.push(n); //直接把当前根节点推入栈
// 如果根节点存在的话,我们就把它推入函数的调用堆栈中
// 因为先序遍历倒序过来和后序遍历的左右孩子的顺序不太对,所心得换一下左右孩子的进栈顺序
if (n.left) {//在根节点的左孩子有值的情况下
stack.push(n.left);//left进栈
}
if (n.right) {
/在根节点的右孩子有值的情况下
stack.push(n.right);//right进栈
}
//全部是根节点先入倒置的那个栈
}
// 倒序输出
while (outputStack.length) {
const n = outputStack.pop();
console.log(n.val);
}
};
postorder(bt);//调用后序遍历的方法,把二叉树丢进去
8.5 LeetCode: 104. 二叉树的最大深度
解题思路:
- 求二叉树的最大深度,考虑使用深度优先遍历
- 在深度优先遍历过程中,记录每个节点所在的层级,找出最大层级
解题步骤:
- 新建变量,记录最大深度
- 深度优先遍历整棵树,并记录每个节点的层级,同时不断更新最大深度这个变量
代码实现:
法一:
/**
* 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) {
let res = 0;
const dfs = (n, l) => {
if (!n) { return; }
res = Math.max(res, l);
dfs(n.left, l + 1);
dfs(n.right, l + 1);
};
dfs(root,1);
return res;
};
代码解读:
- 先对二叉树进行了深度优先遍历:
- 获取每个节点的层级数:
法二:
代码实现:
/**
* 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) {
var res = 0;
var dfs = (n, l) => {
if (!n) { return; }
if (!n.left && !n.right) {
res = Math.max(res, l);
}
dfs(n.left, l + 1);
dfs(n.right, l + 1);
};
dfs(root, 1);
return res;
};
代码解读:
判断n是否是叶子节点,如果是则刷新最大的层级数: 如果当前的左节点为空并且右节点也为空,则刷新最大的层级数。
- 时间复杂度:O(n) n是整棵树的节点数,深度遍历循环了n次
- 空间复杂度:O(logn-n) 为二叉树的深度,最好深度为logn,最坏的情况深度为n
综上:法一效率更高些
8.6 LeetCode: 111. 二叉树的最小深度
代码实现:
/**
* 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 minDepth = function (root) {
if (!root) { return 0; }
var q = [[root, 1]];
while (q.length) {
var [n, l] = q.shift();
if (!n.left && !n.right) {
return l;
}
if (n.left) q.push([n.left, l + 1]);
if (n.right) q.push([n.right, l + 1]);
}
};
代码解读:
整棵树的广度优先遍历:
记录访问的每个节点的层级数。
8.7 LeetCode: 102. 二叉树的层序遍历
法一:
代码实现:
/**
* 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 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);
}
return res;
};
代码解读:
整棵树的广度优先遍历:
记录访问的每个节点的层级数。
法二:
代码实现:
/**
* 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 levelOrder = function (root) {
if (!root) return [];
var q = [root];
var res = [];
while (q.length) {
let len = q.length;
res.push([]);
while (len--) {
var 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;
};
8.8 LeetCode: 94. 二叉树的中序遍历
递归版:
代码实现:
/**
* 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 inorderTraversal = function (root) {
var res = [];
var rec = (n) => {
if (!n) return;
rec(n.left);
res.push(n.val);
rec(n.right);
};
rec(root);
return res;
};
迭代版:
代码实现:
/**
* 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 inorderTraversal = function (root) {
var res = [];
var stack = [];
let p = root;
while (stack.length || p) {
while (p) {
stack.push(p);
p = p.left;
}
var n = stack.pop();
res.push(n.val);
p = n.right;
}
return res;
};
综上:迭代版的算法比递归版的算法效率要高。
8.9 LeetCode: 112. 路径总和
代码实现:
/**
* 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
* @param {number} targetSum
* @return {boolean}
*/
// 路径总和
var hasPathSum = function (root, sum) {
if (!root) return false;
let res = false;
var 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;
};
代码解读:
深度遍历二叉树:
记录每条路径值的总各和:
判断路径值是否和目标值相同:
8.10 前端与树:遍历JSON的所有节点
新建一个JSON
拿到所有节点的值:
可以知道值所属的节点:
8.11 前端与树:渲染Antd中的树组件
利用深度优先算法来渲染,在前端中经常用。