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

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

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

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

模拟的数据如下所示:

 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 });

ic_transfer_5.png

树转列表

深度优先递归

 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 };

ic_transfer_6.png

循环实现

 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

ic_transfer_7.png

实际函数应用

页面展示

 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);

ic_tree_4.png

选中节点禁用

 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"]);

ic_tree_5.png

选中节点过滤

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

 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 );

ic_tree_6.png

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

 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 };

ic_tree_9.png

选中节点保留

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

 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 );

ic_tree_7.png

关键词过滤

 

 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 };

ic_tree_8.png

posted @ 2021-09-15 18:39  Grails  阅读(153)  评论(0编辑  收藏  举报
Live2D