“树”结构数据常用操作汇总
“树”结构数据常用操作汇总
主要介绍了树结构数据的一些常用操作函数,如树的遍历、树转列表、列表转树、树节点查找、树节点路径查找等等。
结合到实际项目中,进行了一些变化处理,如穿梭框组件的封装,本质上是对数据的处理,对数据的过滤以及状态的更改等,反映到页面上展示。
模拟的数据如下所示:
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 };
循环实现
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
实际函数应用
页面展示
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);
选中节点禁用
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"]);
选中节点过滤
源数据框数据处理,过滤掉选中节点,不展示
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 );
有时候,当父节点满足条件,但是没有满足条件的子节点时,也要正常返回数据。上面的方法就不符合条件了,改成如下实现了。
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 };
选中节点保留
目标数据处理,仅仅展示选中节点,其他数据过滤掉
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 );
关键词过滤
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 };