1.树介绍:
树(英语:tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据集合。 它是由n(n>=1)个有限节点组成一个具有层次关系的集合。 把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
二叉树:每一个节点最多只有两个子节点;
完全二叉树:特殊的二叉树,完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树;
二叉搜索树(BST、二叉查找树):节点的左子节点小于父节点,节点的右子节点大于父节点。
红黑树:就是一种平衡二叉树,说它平衡的意思是它不会出现左子树与右子树的高度之差不会大于1,左子树和右子树保持一种平衡的关系。每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。
2.递归处理
在开始将树之前必须要先说一下树的递归处理,将从后端的数据转换为统一格式的结构。
function handleTreeData(data) {
let arr = [];
data.forEach((item) => {
arr.push({
...item,
title: item.name,
key: item.id,
children:
item.children && item.children.length
? handleTreeData(item.children)
: [],
});
});
return arr;
}
let treedata = [
{
name: "1-1",
id: 1,
parentId: 0,
children: [
{
name: "1-1-1",
id: 2,
parentId: 1,
},
],
}
];
3.遍历树:
树结构的常用场景之一就是遍历,而遍历又分为广度优先遍历、深度优先遍历。 其中深度优先遍历是可递归的,而广度优先遍历是非递归的,通常用循环来实现。 深度优先遍历又分为先序遍历、后序遍历,二叉树还有中序遍历,实现方法可以是递归,也可以是循环。
广度优先(BFC):
BFS是从根节点开始,沿着树的宽度遍历树的节点。 如果所有节点均被访问,则算法中止。
let tree = [
{
id: "1",
title: "节点1",
children: [
{
id: "1-1",
title: "节点1-1",
},
{
id: "1-2",
title: "节点1-2",
},
],
},
{
id: "2",
title: "节点2",
children: [
{
id: "2-1",
title: "节点2-1",
},
],
},
];
// 广度优先
function treeForeach(tree, func) {
let node;
let list = [...tree];
while ((node = list.shift())) {
func(node);
node.children && list.push(...node.children);
//每次将节点的children元素push到list后面,不断循环,知道最后一层
}
}
treeForeach(tree, (node) => {
console.log(node.title);
});
深度优先(DFC):
这三种遍历算法可以参考网上详细内容
4.树形数据结构化
一般是运用的知识是函数、递归、数组的常用操作,学习树形结构的基本算法,这样便于以后我们可以更好的处理业务逻辑。
1.列表转换为树:
一个扁平数据列表,我们找到对应的关键字段,比如parentId和id的对应关系;
let list = [
{
id: "1",
title: "节点1",
parentId: "",
},
{
id: "1-1",
title: "节点1-1",
parentId: "1",
},
{
id: "1-2",
title: "节点1-2",
parentId: "1",
},
{
id: "2",
title: "节点2",
parentId: "",
},
{
id: "2-1",
title: "节点2-1",
parentId: "2",
},
{
id: "3-1",
title: "节点3-1",
parentId: "2-1",
},
];
//网上简易版
function listToTree(list) {
let info = list.reduce(
(map, node) => ((map[node.id] = node), (node.children = []), map),
{}
);
return list.filter((node) => {
info[node.parentId] && info[node.parentId].children.push(node);
return !node.parentId;
});
}
// console.log(listToTree(list));
//新手理解版
function formatDataTree(list) {
let parents = list.filter((p) => p.parentId === "");
let childrens = list.filter((c) => c.parentId !== "");
dataToTree(parents, childrens);
return parents;
function dataToTree(parents, childrens) {
parents.map((p) => {
childrens.map((c, i) => {
if (c.parentId === p.id) {
let _children = JSON.parse(JSON.stringify(childrens));
_children.splice(i, 1);
dataToTree([c], _children);
if (p.children) {
//判断父元素是否存在children
p.children.push(c);
} else {
p.children = [c];
}
}
});
});
}
}
console.log(formatDataTree(list));
2.树转换为列表:
常用在使用表格来展示树,之前是后端帮我们处理好,前端也可以自己做。
let tree = [
{
id: "1",
title: "节点1",
children: [
{
id: "1-1",
title: "节点1-1",
},
{
id: "1-2",
title: "节点1-2",
},
],
},
{
id: "2",
title: "节点2",
children: [
{
id: "2-1",
title: "节点2-1",
},
],
},
];
//方法一:递归实现
function treeToList(tree, result = [], level = 0) {
tree.forEach((node) => {
result.push(node);
node.level = level + 1;
node.children && treeToList(node.children, result, level + 1);
});
return result;
}
console.log(treeToList(tree))
// 方法二:循环实现
// function treeToList (tree) {
// let node, result = tree.map(node => (node.level = 1, node))
// for (let i = 0; i < result.length; i++) {
// if (!result[i].children) continue
// let list = result[i].children.map(node => (node.level = result[i].level + 1, node))
// result.splice(i+1, 0, ...list)
// }
// return result
// }
//方法三:网上简易版
function treeToList(tree) {
return (tree || []).reduce((total, current) => {
const { children, ...obj } = current;
return total.concat(obj, treeToList(children));
}, []);
}
5.树结构查找
查找节点:
查找节点其实就是一个遍历的过程,遍历到满足条件的节点则返回,遍历完成未找到则返回null。类似数组的>find方法,传入一个函数用于判断节点是否符合条件,这里我们使用的是深度优先,代码如下: 应用场景:目录树搜索功能。
下面是从:深度优先和广度优先两个方面讲。
let tree = [
{
id: "1",
title: "节点1",
children: [
{
id: "1-1",
title: "节点1-1",
},
{
id: "1-2",
title: "节点1-2",
},
],
},
{
id: "2",
title: "节点2",
children: [
{
id: "2-1",
title: "节点2-1",
},
],
},
];
//深度优先
function treeFind(tree, func) {
for (const data of tree) {
if (func(data)) return data; //有就直接返回
if (data.children) { //没有就进去子节点查找,递归调用
const res = treeFind(data.children, func);
if (res) return res;
}
}
return null; //都没找到,返回null
}
function findsingle(data) {
if (data.title === "节点1-2") {
return true;
}
}
console.log(treeFind(tree,findsingle)) //{ id: '1-2', title: '节点1-2' }
//广度优先
function treeFind(tree, func) {
for (const data of tree) {
if (func(data)) return data //直接循环第一层的所有节点
}
const chidrens = tree.reduce((total, current) => {
return total.concat(current.children || []);
}, []);
const res = treeFind(chidrens, func);
if (res) return res;
return null;
}
查找节点路径:
略微复杂一点,因为不知道符合条件的节点在哪个子树,要用到回溯法的思想。查找路径要使用先序遍历,维护>一个队列存储路径上每个节点的id,假设节点就在当前分支,如果当前分支查不到,则回溯。
let tree = [
{
id: "1",
title: "节点1",
children: [
{
id: "1-1",
title: "节点1-1",
},
{
id: "1-2",
title: "节点1-2",
},
],
},
{
id: "2",
title: "节点2",
children: [
{
id: "2-1",
title: "节点2-1",
},
],
},
];
function treeFindPath(tree, func, path = []) {
if (!tree) return []; //空判断
for (const data of tree) {
path.push(data.id);
if (func(data)) return path;
if (data.children) {
const findChildren = treeFindPath(data.children, func, path);
if (findChildren.length) return findChildren;
}
path.pop();
}
return [];
}
function findsingle(data) {
if (data.title === "节点1-2") {
return true;
}
}
console.log(treeFindPath(tree, findsingle, (path = []))) //[ '1', '1-2' ]
查找多条节点路径
思路与查找节点路径相似,不过代码却更加简单:
let tree = [
{
id: "1",
title: "节点1",
children: [
{
id: "1-1",
title: "节点1-1",
},
{
id: "1-2",
title: "节点1-2",
},
],
},
{
id: "2",
title: "节点2",
children: [
{
id: "2-1",
title: "节点2-1",
},
],
},
];
function treeFindPath(tree, func, path = [], result = []) {
for (const data of tree) {
path.push(data.id);
func(data) && result.push([...path]);
data.children && treeFindPath(data.children, func, path, result);
path.pop();
}
return result;
}
function findsingle(data) {
if (data.title === "节点1-2") {
return true;
}
}
console.log(treeFindPath(tree, findsingle)) //[ [ '1', '1-2' ] ]
6.其他常用算法
树的最大深度
求树的最大深度是写拖拽的时候,用到的;使用场景,实现最多5层拖拽树,当拖动节点到当前节点下的时候, 需要判断拖拽之后会不会超过5层,这是时候需要计算拖拽节点的深度和当前节点的深度,不能超过5.
let treedata = {
name: "1-1",
id: 1,
parentId: 0,
children: [
{
name: "1-1-1",
id: 2,
parentId: 1,
children: [],
},
{
name: "1-1-3",
id: 4,
parentId: 1,
children: [
{
name: "1-1-3-1",
id: 5,
parentId: 4,
children: [],
},
{
name: "1-1-3-2",
id: 6,
parentId: 4,
children: [],
},
],
},
],
};
maxDepth = (root) => {
if (root === null) {
return 0;
}
var max = 0;
for (var i = 0; i < root.children.length; i++) {
max = Math.max(max, maxDepth(root.children[i]));
}
return max + 1;
};
树的总节点个数
应用场景:主要针对树的展开层级,如果节点比较多的话,全部展开的话 页面效果不好,根据当前页面的高度 和节点个数,看展开的层级。
let treedata = [
{
name: "1-1",
id: 1,
parentId: 0,
children: [
{
name: "1-1-1",
id: 2,
parentId: 1,
children: [],
},
{
name: "1-1-3",
id: 4,
parentId: 1,
children: [
{
name: "1-1-3-1",
id: 5,
parentId: 4,
children: [],
},
{
name: "1-1-3-2",
id: 6,
parentId: 4,
children: [],
},
],
},
],
},
];
let leafcount = 0;
function treeNodecount(data) {
let arr = [];
data.forEach((item) => {
leafcount++;
arr.push({
children:
item.children && item.children.length
? treeNodecount(item.children)
: [],
});
});
return arr;
}
treeNodecount(treedata)
console.log(leafcount)
树结构筛选
树结构过滤即保留某些符合条件的节点,剪裁掉其它节点。一个节点是否保留在过滤后的树结构中,取决于它以>及后代节点中是否有符合条件的节点。可以传入一个函数描述符合条件的节点:
let tree = [
{
id: "1",
title: "节点1",
children: [
{
id: "1-1",
title: "节点1-1",
},
{
id: "1-2",
title: "节点1-2",
},
],
},
{
id: "2",
title: "节点2",
children: [
{
id: "2-1",
title: "节点2-1",
},
],
},
];
function treeFilter(tree, func) {
// 使用map复制一下节点,避免修改到原树
return tree
.map((node) => ({ ...node }))
.filter((node) => {
node.children = node.children && treeFilter(node.children, func);
return func(node) || (node.children && node.children.length);
});
}
function findsingle(data) {
if (data.title === "节点1") {
return true;
}
}
console.log(treeFilter(tree,findsingle))
// {
// id: "1"
// title: "节点1",
// children:[]
// }
根据条件从树形结构中解析数据:
应用场景:在一堆的产品客户层级中,找到我们想要的类型的数据,返回的数据是平级的,这样可以使用表格或者其他来展示出我们想要看的结果。
let tree = [
{
name: "1-1",
id: 1,
parentId: 0,
type: "user",
children: [
{
name: "1-1-1",
id: 2,
parentId: 1,
children: [],
type: "manager",
},
{
name: "1-1-3",
id: 4,
parentId: 2,
type: "user",
children: [
{
name: "1-1-3-1",
id: 5,
parentId: 4,
children: [],
type: "manager",
},
{
name: "1-1-3-2",
id: 5,
parentId: 4,
children: [],
type: "manager",
}
],
},
],
},
];
function parseTreeData(tree) {
return (tree || []).reduce((total, current) => {
const { type, children, ...obj } = current;
if (type === "manager") {
return total.concat(obj);
}
const paserChildren = parseTreeData(children);
return total.concat(paserChildren);
}, []);
}
console.log(parseTreeData(tree));
往树中增、删、改、查节点:
这个相对比较简单,例如插入思路:获取当前节点,然后将被插入的节点作为当前节点的children数据就可以了。 其他删、改、查都是运用递归处理树数据,在递归的过程中,做一些操作。
最后:
如果你对树的算法很感兴趣,可以点开下面的地址,学习更多的算法知识。 树-leetcode 刷题链接leetcode-cn.com/tag/tree/