当前位置: 首页 > Web前端 > JavaScript

树,计算父节点的值

时间:2023-03-27 17:59:54 JavaScript

前段时间回答过类似的问题,萌生了写博客的想法。这个问题确实有一些常见的应用场景。例如,在一个多层次的组织结构中,每个员工的绩效得分是已知的,希望计算出各级部门的绩效得分,以便对部门进行评价。根据这个描述,准备一个测试数据如下:","children":[{"name":"软件部"},{"name":"测试部"}]},{"name":"工程部","children":[{"name":"预算部"},{"name":"项目管理部"}]}]},{"name":"财务序列","children":[{"name":"财务部"},{"name":"Office"}]}]}树形直观表示如下├──生产顺序│├──研发部│├──软件组││└──测试组│└──工程部│├──预算组│└──项目管理组└──财务线├──财务部└──办公室每个部门都有几名员工。假设获取的数据用元组(数组)表示,即[姓名、部门、??职位、个人业绩],下面是一个随机生成的用例数据。[["SuYanchang","ProductionSequence","Director",81],["TaoYaozhong","R&DDepartment","Minister",80],["JiYimeng","SoftwareGroup","ChiefEngineer",87],["QiangHangdong","SoftwareGroup","Analyst",84],["ChuChenzhang","SoftwareGroup","Architect",96],["WuHaomiao","SoftwareGroup","Engineer",95],["TangJinjing","SoftwareGroup","Engineer",87],["PengLizi","SoftwareGroup","Engineer",91],["MuQianxia","SoftwareGroup","Engineer",84],["LvChengtong","SoftwareGroup","Engineer",86],["HuaNingxuan","TestGroup","TestTeamLeader",93],["MiaoKuiwei","TestGroup","TestEngineer",95],["ZouLianjuan","TestGroup","TestEngineer",89],["QinHuishu","TestGroup","Assistant",85],["GuNuochang","EngineeringDepartment","Minister",86],["YuMengxun","BudgetGroup","BudgetEngineer",91],["ZhenLinDay",《预算组》《预算工程师》,81],[《刘梦莹》,《预算组》,《预算工程师》,92],[《闫宁莹》,《项目管理组》,《项目经理》,84],[《严凌天》,《项目管理组》,《项目经理》,81],[《史标龙》,《项目管理组》,《项目经理》,95],[《龚同品》,《财务序列》,《处长》,95],[《魏晓燕》,《财务部》,《部长》,85],[《程玄彤》,《财务部》,《会计??》,88],[《志语诗》,《财务部》,《出纳》,92],[《易洪权》,《办公室》,《主任》,83],[《丁艳莲》,《办公室》,《助理》,82]]计算每个部门的绩效分数。假设获取的部门数组存放在depts变量中,员工绩效数组存放在staffs变量中。计算各部门绩效分数的规则采用了最简单的算法:该部门所有员工绩效分数的平均值,但仍然有几种处理方式。合并大树,将depts和staffs合并成一个orgTree,然后递归计算;直接在depts上递归计算,到了department再去staffs中找到该部门的employee,根据子部门的score计算出部分performancescore。方法一,组合大树即使是组合大树,也有很多种方法。遍历staffs,根据每个员工的部门找到正确的树节点,并添加。由于查询树节点算法比较复杂,这种方式搜索起来比较慢;首先从depts生成部门名称到部门对象的映射,然后遍历staffs找到部门节点。这种方法使用映射表,查找速度快,内存消耗稍大。现在内存不贵,有些数组能消耗的内存极其有限,所以采用第二种方法。生产映射表需要遍历树。我曾经在使用递归遍历和转换树数据一文中介绍过几种遍历树的方法。这是另一个。使用Generator(本质上是深度递归)来熟悉yield和yield*的用法。functionflatTree(nodes){//兼容单节点和节点数组(单根/多根)if(!Array.isArray(nodes)){nodes=[nodes];}//一切从这里开始return[...迭代(节点)];//内部迭代生成器递归实现函数*iterate(nodes){for(constnodeofnodes){yieldnode;如果(node.children?.length){yield*iterate(node.children);}}}}Map可以作为映射表,但是由于key(??部门名称)是文本,那么直接使用JS对象作为映射表,Object.fromEntries()安排:constdeptMap=Object.fromEntries(flatTree(depts).map(node=>[node.name,node]));注意:这个映射表的前提是每个部门的名字都不一样。如果存在同名的部门,则需要找到另一个唯一标识属性,通常是ID。合并员工数据时,需要为每个节点添加staffs属性。为了不污染源depts数据,使用JSON做一个简单的深拷贝constorgTree=JSON.parse(JSON.stringify(depts));constdeptMap=Object.fromEntries(flatTree(orgTree).map(node=>[node.name,node])//^^^^^^^);合并员工:遍历员工列表,添加到树节点一个接一个。注意这里每个员工都是一个元组(数组),所以用解构的方式快速获取每个属性也方便后面直接形成对象。staffs.forEach(([name,dept,title,value])=>{constdeptNode=deptMap[dept];//如果找不到部门就忽略,虽然样本数据中不存在这种情况,但应该是合适的写业务时容错if(!deptNode){return;}(deptNode.staffs??=[]).push({name,dept,title,value});});最后,是性能。通常,部门绩效是其下所有员工绩效的平均值。然后你需要计算总分和它下面所有员工的员工数。注意,递归计算时,上级部门需要用下级部门的总分和总人数,而不是平均分——为什么?别问我,问数学老师!下面的calcValue还是一个入口,里面的calcNode和calcNodeList是递归函数。注意这里拆分了处理单个部门和处理部门数组的逻辑(每个部门的子部门是一个部门数组,每个部门数组又包含几个单独的部门),calcNode和calcNodeList是双递归实现,相互调用关系。函数calcValue(deptNodes){Array.isArray(deptNodes)?计算节点列表(部门节点):计算节点(部门节点);/***@returns返回[总人数,总分](因为计算过程中只需要这两个值)*/functioncalcNodeList(depts){returndepts.reduce(([sum,count],dept)=>{const[deptCount,deptSum]=calcNode(dept);//递归求和+=deptSum;count+=deptCount;return[sum,count];},[0,0]);}/***@returns[总人数,总分](因为计算过程中只需要这两个值)*/functioncalcNode(dept){let[count,sum]=[0,0];//如果有子系,先统计子系if(dept.children?.length){//计算时不关心子系的平均分const[deptSum,deptCount]=calcNodeList(dept.children);//递归和+=deptSum;计数+=部门计数;}//必须添加if(dept.staffs?.length){sum+=dept.staffs.reduce((sum,{value})=>sum+value,0);count+=dept.staffs.length;}//不要忘记0不是除数Object.assign(dept,{sum,count,value:count&&(sum/count)});返回[dept.count,dept.sum];}}不要害怕双递归,多重递归...把一个递归拆分成多个函数,还是把一个大逻辑拆分成几个小逻辑,自然拆分即可,不需要特别注意是否调用方法二递归,保持部门和员工的数据分离方法一中,递归计算的calcNode函数中有一段是计算员工的,就是这一段:if(dept.staffs?.length){sum+=dept.staffs.reduce((sum,{value})=>sum+value,0);count+=dept.staffs.length;}考虑把它封装成一个函数调用://IIFE调用,在一个句子中处理两个结果数据(([c,s])=>[count,sum]=[count+c,sum+s])(calcStaffs(dept));//^^^^^^解构传入参数元组^^^^^^^^^^^^^^^^^结果是一个元组,如一个参数//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^使用解构表达式计算并重新赋值//计算部门总分和员工总人数functioncalcStaffs(dept){if(!dept.staffs?.length){return[0,0];}返回[dept.staffs.length,dept.staffs.reduce((sum,{value})=>sum+value,0),];}上面的IIFE调用将临时变量c和s密封在一个小的局部范围内。也就是扔掉,然后用解构把两条赋值语句写成一句话。如果你不习惯,像count+=c那样隔开肯定不会错。注意calcStaffs的参数是一个部门,该函数使用dept.staffs查找该部门当前的员工。如果我们直接根据dept.name在原来的staffs数组中查找数据,就不需要提前将员工分配到对应的部门。例如,这样查找:functioncalcStaffs(dept){constdeptStaffs=staffs.filter(([,deptName])=>dept.name===deptName);//^^^^^^^^^^^^^^从所有员工中按部门名称过滤员工if(!deptStaffs.length){return[0,0];}//^不再需要?因为过滤结果必须是数组,但也可能为空return[deptStaffs.length,deptStaffs.reduce((sum,[,,,value])=>sum+value,0),//^^^^^^^^^^^^^注意原来staffs中的每个员工都是Tuple的意思];}这样直接从staffs这个包含所有员工的数组中查找员工,就不用再往departments中添加员工了提前,所以之前的staffs.forEach()就不需要了……嗯,就是这段:staffs.forEach(([name,dept,title,value])=>{...});当员工很多的时候,确实会影响每次过滤器遍历的效率。在这种情况下,您可以先对员工进行分组以获得staffMapconststaffMap=staffs.reduce((all,staff)=>{(all[staff[1]]??=[]).push(staff);returnall;},{});当然也不需要使用filter来查找,直接查表即可。修改难度不大,代码省略。注意搜索结果中有undefined的可能!如果你不明白我为什么这么说,只要看看上面第3段代码中的注释即可。动态计算至此,我们在准备好所有数据的情况下进行了静态计算。如果在UI上填写分数,需要实时计算各部门的绩效分数怎么办?那么就需要进行动态计算——当然,当某个分数发生变化时,重新计算所有节点肯定不会错,但是会浪费算力。分析一下,如果员工所在的分支机构发生变化,可能会产生什么影响?一定会影响他部门分数的连锁反应吗,会不会影响本部门的父部门,祖先部门的分数会不会影响子部门的分数?惯于!会不会影响兄弟系的分数?也不会!所以,这种情况下,只需要找到分数发生变化的员工的部门,以及他的父部门,从下往上一步步重新计算即可。在计算各层级部门的分数时,其子层级的分数已经固定(计算完成),因此无需递归,只需使用直属子级和直属员工的分数计算即可。不过看上面的结构,每个树节点都没有父关联,所以查找部门还是要从根开始,递归查找。关于递归查找的问题,在filter/filtertreenode中已经说明,不再赘述。也就是说有家长会的时候比较简单,这里就不多说了。计算过程比较简单,因为没有递归//路径是从根节点到员工部门节点的集合functioncalcPath(path){//我们需要从靠近叶节点的节点开始计算,所以先反转方向//注意reverse会改变原来的数组path.reverse();for(constdeptofpath){[dept.count,dept.sum]=dept.children?.reduce(([count,sum],it)=>[count+it.count,sum+it.sum],[0,0])??[0,0];//没有孩子的时候给一个默认值//只有有员工并且数组不为空时才处理dept.staffs?.length&&(//reduce过程和上面类似,只是取值不同[dept.count,dept.sum]=dept.staffs.reduce(([count,sum],it)=>[count+1,sum+it.value],//只有count和sum已经有值,需要从已有值中累加[dept.count,dept.sum]));}}小结根据子节点计算父节点值本质上是基于遍历树节点知识点的应用,同样需要掌握集合数据的处理方法。至于语法,不要害怕使用新的语法,也不要太担心兼容性问题。与旧环境的兼容性可以由编译器处理(如tsc、babel等)。最后推荐几篇相关的博文:Userecursivetraversalandconversionoftreedatatofilter/filtertreenodestogeneratetreesfromlists(JavaScript/TypeScript)JavaScript数据处理-映射表JavaScript数据处理-列表