“树”结构数据常用操作汇总




主要介绍了树结构数据的一些常用操作函数,如树的遍历、树转列表、列表转树、树节点查找、树节点路径查找等等。

结合到实际项目中,进行了一些变化处理,如穿梭框组件的封装,本质上是对数据的处理,对数据的过滤以及状态的更改等,反映到页面上展示。

模拟的数据如下所示:



1 const provinceList = [
2 {
3 id: "1000",
4 label: "湖北省",
5 children: [
6 {
7 id: "1001",
8 pid: "1000",
9 label: "武汉",
10 children: [
11 { id: "100101", pid: "1001", label: "洪山区" },
12 { id: "100102", pid: "1001", label: "武昌区" },
13 { id: "100103", pid: "1001", label: "汉阳区" },
14 ],
15 },
16 { id: "1020", pid: "1000", label: "咸宁" },
17 { id: "1022", pid: "1000", label: "孝感" },
18 { id: "1034", pid: "1000", label: "襄阳" },
19 { id: "1003", pid: "1000", label: "宜昌" },
20 ],
21 },
22 {
23 id: "1200",
24 value: "江苏省",
25 label: "江苏省",
26 children: [
27 { id: "1201", pid: "1200", label: "南京" },
28 { id: "1202", pid: "1200", label: "苏州" },
29 { id: "1204", pid: "1200", label: "扬州" },
30 ],
31 },
32 ];


树的遍历

深度优先遍历



1 /**
2 * 深度优先遍历
3 * @params {Array} tree 树数据
4 * @params {Array} func 操作函数
5 */
6 const dfsTransFn = (tree, func) => {
7 tree.forEach((node) => {
8 func(node);
9 // 如果子树存在,递归调用
10 if (node.children?.length) {
11 dfsTransFn(node.children, func);
12 }
13 });
14 return tree;
15 };
16
17 // 打印节点
18 dfsTransFn(tree, (node) => {
19 console.log(`${node.id}...${node.value}`);
20 });


深度循环遍历

与广度优先类似,要维护一个队列。不过本函数是加到队列最前面,而广度优先遍历是加到队尾。



1 const dfsTreeFn = (tree, func) => {
2 let node,
3 list = [...tree];
4 // shift()-取第一个
5 while ((node = list.shift())) {
6 func(node);
7 // 如果子树存在,递归调用
8 // 子节点追加到队列最前面`unshift`
9 node.children && list.unshift(...node.children);
10 }
11 };


广度优先遍历



1 /**
2 * 广度优先遍历
3 * @params {Array} tree 树数据
4 * @params {Array} func 操作函数
5 */
6 const bfsTransFn = (tree, func) => {
7 let node,
8 list = [...tree];
9 // shift()-取第一个;pop()-取最后一个
10 while ((node = list.shift())) {
11 func(node);
12 // 如果子树存在,递归调用
13 node.children && list.push(...node.children);
14 }
15 };
16
17 // 打印节点
18 bfsTransFn(tree, (node) => {
19 console.log(`${node.id}...${node.value}`);
20 });


“树”结构数据常用操作汇总_数据

树转列表

深度优先递归



1 const dfsTreeToListFn = (tree, result = []) => {
2 if (!tree?.length) {
3 return [];
4 }
5 tree.forEach((node) => {
6 result.push(node);
7 console.log(`${node.id}...${node.label}`); // 打印节点信息
8 node.children && dfsTreeToListFn(node.children, result);
9 });
10 return result;
11 };


广度优先递归



1 const bfsTreeToListFn = (tree, result = []) => {
2 let node,
3 list = [...tree];
4 while ((node = list.shift())) {
5 result.push(node);
6 console.log(`${node.id}...${node.label}`); // 打印节点信息
7 node.children && list.push(...node.children);
8 }
9 return result;
10 };


“树”结构数据常用操作汇总_时间复杂度_02

循环实现



1 export const treeToListFn = (tree) => {
2 let node,
3 result = tree.map((node) => ((node.level = 1), node));
4 for (let i = 0; i < result.length; i++) {
5 // 没有子节点,跳过当前循环,进入下一个循环
6 if (!result[i].children) continue;
7 // 有子节点,遍历子节点,添加层级信息
8 let list = result[i].children.map(
9 (node) => ((node.level = result[i].level + 1), node)
10 );
11 // 将子节点加入数组
12 result.splice(i + 1, 0, ...list);
13 }
14 return result;
15 };


列表转树



1 const listToTreeFn = (list) => {
2 // 建立了id=>node的映射
3 let obj = list.reduce(
4 // map-累加器,node-当前值
5 (map, node) => ((map[node.id] = node), (node.children = []), map),
6 // 初始值
7 {}
8 );
9 return list.filter((node) => {
10 // 寻找父元素的处理:
11 // 1. 遍历list:时间复杂度是O(n),而且在循环中遍历,总体时间复杂度会变成O(n^2)
12 // 2. 对象取值:时间复杂度是O(1),总体时间复杂度是O(n)
13 obj[node.pid] && obj[node.pid].children.push(node);
14 // 根节点没有pid,可当成过滤条件
15 return !node.pid;
16 });
17 };


查找节点

判断某个节点是否存在,存在返回 true,否则返回 false



1 const treeFindFn = (tree, func) => {
2 for (let node of tree) {
3 if (func(node)) return node;
4 if (node.children) {
5 let result = treeFindFn(node.children, func);
6 if (result) return result;
7 }
8 }
9 return false;
10 };
11
12 // 测试代码
13 let findFlag1 = treeFindFn(provinceList, (node) => node.id === "1020");
14 let findFlag2 = treeFindFn(provinceList, (node) => node.id === "100125");
15 console.log(`1020 is ${JSON.stringify(findFlag1)}, 100125 is ${findFlag2}`);
16
17 // 打印结果:
18 1020 is {"id":"1020","pid":"1000","label":"咸宁","key":"1020","title":"咸宁","level":2,"children":[]}, 100125 is null


查找节点路径



1 const treeFindPathFn = (tree, func, path = []) => {
2 if (!tree) return [];
3
4 for (let node of tree) {
5 path.push(node.id);
6 if (func(node)) return path;
7 if (node.children) {
8 const findChild = treeFindPathFn(node.children, func, path);
9 if (findChild.length) return findChild;
10 }
11 path.pop();
12 }
13 return [];
14 };
15
16 // 测试代码
17 let findPathFlag = treeFindPathFn(
18 provinceList,
19 (node) => node.id === "100102"
20 );
21 console.log(`100102 path is ${findPathFlag}`);
22
23 // 打印结果
24 100102 path is 1000,1001,100102


“树”结构数据常用操作汇总_时间复杂度_03

实际函数应用

页面展示



1 <template>
2 <div class="demo-block">
3 <div class="demo-block-title">穿梭框数据处理函数:</div>
4 <div class="demo-block-content">
5 <div class="demo-block-title">原数据:</div>
6 <a-tree blockNode checkable defaultExpandAll :tree-data="provinceData" />
7 </div>
8 <div
9 class="demo-block-content"
10 style="margin-left: 40px;vertical-align: top;"
11 >
12 <div class="demo-block-title">处理后数据:filterSourceTreeFn</div>
13 <a-tree
14 blockNode
15 checkable
16 defaultExpandAll
17 :tree-data="optProvinceData"
18 />
19 </div>
20 </div>
21 </template>
22
23 <script>
24 import * as R from "ramda";
25 import provinceList from "./mock.json";
26 export default {
27 data() {
28 return {
29 provinceData: [],
30 optProvinceData: [],
31 };
32 },
33 };
34 </script>
35
36 <style lang="scss"></style>


数据转化(遍历)

将模拟数据转为组件需要的数据,遍历数据,添加 ​​title​​ 和 ​​key​​ 字段



1 const treeTransFn = (tree) =>
2 dfsTransFn(tree, (o) => {
3 o["key"] = o.id;
4 o["title"] = o.label;
5 });
6
7 this.provinceData = treeTransFn(provinceList);


“树”结构数据常用操作汇总_递归调用_04

选中节点禁用



1 const disabledTreeFn = (tree, targetKeys) => {
2 tree.forEach((o) => {
3 let flag = targetKeys.includes(o.id);
4 o["key"] = o.id;
5 o["title"] = flag ? `${o.label}(已配置)` : o.label;
6 o["disabled"] = flag;
7 o.children && disabledTreeFn(o.children, targetKeys);
8 });
9 return tree;
10 };
11
12 this.provinceData = disabledTreeFn(provinceList, ["100101", "1022", "1200"]);


“树”结构数据常用操作汇总_子节点_05

选中节点过滤

源数据框数据处理,过滤掉选中节点,不展示



1 /**
2 * 选中节点过滤
3 * @params {Array} tree 树数据
4 * @params {Array} targetKeys 选中数据key集合
5 * 过滤条件是:当前节点且其后代节点都没有符合条件的数据
6 */
7 const filterSourceTreeFn = (tree = [], targetKeys = [], result = []) => {
8 R.forEach((o) => {
9 // 1. 判断当前节点是否含符合条件的数据:是-继续;否-过滤
10 if (targetKeys.indexOf(o.id) < 0) {
11 // 2. 判断是否含有子节点:是-继续;否-直接返回
12 if (o.children?.length) {
13 // 3. 子节点递归处理
14 o.children = filterSourceTreeFn(o.children, targetKeys);
15 // 4. 存在子节点,且子节点也有符合条件的子节点,直接返回
16 if (o.children.length) result.push(o);
17 } else {
18 result.push(o);
19 }
20 }
21 }, tree);
22 return result;
23 };
24
25 this.optProvinceData = treeTransFn(
26 filterSourceTreeFn(R.clone(provinceList), ["100101", "1022", "1200"])
27 );


“树”结构数据常用操作汇总_子节点_06

有时候,当父节点满足条件,但是没有满足条件的子节点时,也要正常返回数据。上面的方法就不符合条件了,改成如下实现了。



1 export const filterSourceTreeFn = (tree = [], targetKeys = []) => {
2 return R.map(
3 (o) => {
4 // 2. 存在子节点,递归处理
5 if (o.children?.length) {
6 o.children = filterSourceTreeFn(o.children, targetKeys) || [];
7 }
8 return o;
9 },
10 // 1. 过滤不符合条件的数据
11 R.filter(
12 (r) => targetKeys.indexOf(r.id) < 0,
13 // 避免直接修改原数据,需要R.clone()处理一下
14 R.clone(tree)
15 )
16 );
17 };


“树”结构数据常用操作汇总_递归调用_07

选中节点保留

目标数据处理,仅仅展示选中节点,其他数据过滤掉



1 // 过滤条件是:当前节点或者是其后代节点有符合条件的数据
2 filterTargetTreeFn = (tree = [], targetKeys = []) => {
3 return R.filter((o) => {
4 // 当前节点符合条件,则直接返回
5 if (R.indexOf(o.id, targetKeys) > -1) return true;
6 // 否则看其子节点是否符合条件
7 if (o.children?.length) {
8 // 子节点递归调用
9 o.children = filterTargetTreeFn(o.children, targetKeys);
10 }
11 // 存在后代节点是返回
12 return o.children && o.children.length;
13 }, tree);
14 };
15
16 this.optProvinceData = treeTransFn(
17 filterTargetTreeFn(R.clone(provinceList), ["100101", "1022", "1200"])
18 );


“树”结构数据常用操作汇总_数据_08

关键词过滤

 



1 export const filterKeywordTreeFn = (tree = [], keyword = "") => {
2 if (!(tree && tree.length)) {
3 return [];
4 }
5 if (!keyword) {
6 return tree;
7 }
8
9 return R.filter((o) => {
10 // 1. 父节点满足条件,直接返回
11 if (o.title.includes(keyword)) {
12 return true;
13 }
14 if (o.children?.length) {
15 // 2. 否则,存在子节点时,递归处理
16 o.children = filterKeywordTreeFn(o.children, keyword);
17 }
18 // 3. 子节点满足条件时,返回
19 return o.children && o.children.length;
20 // 避免修改原数据,此处用R.clone()处理一下
21 }, R.clone(tree));
22 };


“树”结构数据常用操作汇总_子节点_09