菜单树结构处理优化--逻辑栈代替递归栈

目录

前言

一、结构分析

1. 菜单显示结构

2. 逻辑分层

3. 抽象节点

二、概念转化

1. 逻辑栈代替递归栈

2. 结构成员

3.结构流程

4. 栈顶调整

三、代码实施

1. 内部类实体

2. 流程实施

四、小结


前言

菜单保存在数据库中可以满足动态维护的需要。在查询菜单的时候,经常会使用到connect by在数据库中查询出一个树形的菜单数据结构,本质是菜单树的先序遍历List。在返回给前端数据前,会将List处理成一个树形结构(本质是List、Map的多级嵌套)。

后端在处理菜单树形结构的时候,通常会使用递归的方式。在递归调用的时候,需要向下传递完整的数据List和父级菜单id,再回传处理结果List。这就存在重复遍历完整的数据List的问题,可以通过递归调用时切分数据List或传递当前遍历index的方式解决,但是显然不够简洁。同时递归栈的使用也需要额外增加系统内存开销。

在此,对菜单造树方式进行进一步优化,使用逻辑栈代替递归栈,对按先序排列的List进行一次性遍历的同时,借助逻辑栈的辅助(暂存父节点),按后序,从左到右,从下到上将子菜单节点与父节点关联起来,遍历结束即可得到所需的结构数据。

一、结构分析

1. 菜单显示结构

菜单树结构处理优化--逻辑栈代替递归栈_第1张图片

菜单显示结构大概如上图(手码,略丑),前端接收到的数据格式大概如下:

  menuItem: [
	{
      name: "一级菜单1",
      hasChildren: true,
      index: "XXXX",
      children: [
		{
          name: "二级菜单1",
		  hasChildren: true,
          index: "XXXX",
		  children: [
			{
			  name: "三级菜单1",
			  index: "XXXX"
			},
			{
			  name: "三级菜单2",
			  index: "XXXX"
			},
		  ]
		},
		{
          name: "二级菜单2",
          index: "XXXX"
		}
      ]
	},
	{
      name: "一级菜单2",
      hasChildren: true,
      index: "XXXX",
      children: [
		{
          name: "二级菜单21",
          index: "XXXX"
		},
		{
          name: "二级菜单22",
          index: "XXXX"
		}
	  ]
	}
  ],

2. 逻辑分层

按上下级关系分层:

菜单树结构处理优化--逻辑栈代替递归栈_第2张图片

3. 抽象节点

真个菜单无法抽象成一个固定参数结构的对象,但各级菜单项拥有一样的数据结构,可以将每个菜单项抽象成菜单节点对象,上下级关系通过引用指向关联。最终菜单对象只需要存储一级菜单引用集合。

根据前端提供的所需数据结构,每个菜单项都包含name、index参数,hasChildren、children参数只在非底层节点包含,可以看做hasChildren=false、children=[]在底层节点被隐藏了。至此可以确定菜单节点对象的参数结构。

二、概念转化

1. 逻辑栈代替递归栈

旧递归方式如下:

    public List> toMenuTree (Integer rootId, List menuList) {
        List> resultListMap = new ArrayList<>();

        for (MenuVO menu : menuList) {
            if ( (rootId == 0 && null == menu.getParentId()) || Objects.equals(menu.getParentId(), rootId)){
                Map map = new HashMap<>();
                map.put("name", menu.getName());
                map.put("index", menu.getIndex());

                if (menu.hasChirdren) {
                    List> children = toMenuTree(menu.getId(), menuList);
                    map.put("hasChildren", true);
                    map.put("children", toMenuTree(menu.getId(), menuList));
                }
                resultListMap.add(map);
            }
        }
        return resultListMap;
    }

当当前菜单拥有子菜单的时候,当前菜单操作挂起,递归等待孩子节点返回。其实就是儿子找爸爸,爸爸等孩子的过程。

递归全遍历List的方式对数据的顺序要求相对较低,因为每次递归数据普及度高,只要求同级菜单按顺序排列即可。但想要单次遍历完成操作,则需要数据严格按照前序遍历顺序排列,所以一般菜单表结构会引入一个display order栏位,其记录的就是菜单数据项的前序遍历顺序。可以直观地看出,菜单的展示顺序跟前序遍历顺序是一样的:

菜单树结构处理优化--逻辑栈代替递归栈_第3张图片

根据前序遍历顺序先根后左再右的顺序特征,只要存在孩子节点,遍历会一直往树的左边走。参照递归,遍历路径上有孩子的节点会暂时入栈等待,没有孩子的节点直接处理关联到栈顶父节点。可见造树的过程是一个树的后序遍历的过程,从整颗树最左节点到同父最右节点逐个归属到父节点,遍历处理完孩子节点后再进行父节点的后续操作。

清楚结构后,使用LinkedList作为一个轻量级的逻辑栈,链表尾部元素作为栈顶,用来暂存有孩子节点的父节点,等其孩子节点关联完父节点再将该父节点出栈处理。在顶端节点(一级菜单)处理完后,将顶端节点并入到菜单结果集中。

问题引申:

(1)如何确定子节点已经全部遍历完毕

(2)前序遍历与后序遍历不同步的问题:主体遍历到达右子节点结束前序遍历时,造树遍历也只遍历到右子树,未到达父节点完成后序遍历。

2. 结构成员

至此,可以确定需要的菜单类结构成员有:约束传入数据结构的对象、菜单节点对象、存放父节点的逻辑栈、存储顶端节点的集合。

针对1(1)的问题,这里的解决方案是同时在栈中保存存储节点的Id和parentId,在当前遍历节点parentId不等于栈顶节点的Id的时候,代表栈顶节点的子节点已全部遍历完毕。故需要增加一个栈数据对象。

菜单类结构成员:

菜单树结构处理优化--逻辑栈代替递归栈_第4张图片

 Module之所以实现实现Comparable接口是为了进入转化树的流程前,将源数据再进行一次按displayOrder的排序,确保数据顺序。

3.结构流程

首先明确:

(1)当前节点的父节点Id(parentId)为null代表当前是一级菜单节点;

(2)当前节点parentId不等于栈顶节点的Id(rootId)时,代表栈顶节点的子节点已全部遍历完毕;

(3)栈顶调整函数是在完成栈顶节点的子节点遍历后,继续出栈做操作,完成未完成的造树后序遍历,直到栈顶节点为当前节点的父节点。

以下流程图使用伪代码表示(思路按流程走,就不废话了):

菜单树结构处理优化--逻辑栈代替递归栈_第5张图片

 说明:

1. 参数

rootId:栈顶元素Id,等于null时正常情况是栈空

modules:源菜单数据

currModule:当前遍历数据节点

currNode:从currModule抽出的菜单节点

stackTop:指代栈顶元素

2. 流程

modules.sort:源数据按display order排序

currModule.toNode:菜单节点抽象过程

inStack(a):指代a的入栈行为

stackTop.addChild(a):当前节点a与栈顶父节点的关联行为

arrangeStackTop:栈顶调整函数

empty stack:遍历处理完栈中剩下的节点数据,实际上也是栈顶调整函数,传入当前父节点Id参数恒定为null。结束时再调用一遍做收尾

i >= size?:只做一个循环遍历的表示,遍历方式有多种。

4. 栈顶调整

栈顶调整函数是1(2)问题的一个解决,是对栈中节点的一个遍历,以完成后续的后序遍历。流程伪代码示意与3类似。

直接上流程:

菜单树结构处理优化--逻辑栈代替递归栈_第6张图片

说明:

 1. 参数

stack:栈

currParentId:要求栈顶节点Id,null表示处理清空栈节点

stackEnpty:出栈的数据对象

rootId:出栈节点的父节点Id(必须使用出栈节点的parentId,不能使用栈顶节点的Id)

stack.top:栈顶节点

2.流程

stack.pop:出栈

Exception:抛出异常

break:当栈顶节点为要求的节点时,退出循环遍历

currParentId is null && stack.isNotEmpty判定:当传入currParentId为null(要求清空栈),但栈处理完后仍非空的情况下,说明菜单display order有误或其他未预知格式问题,抛出异常

三、代码实施

1. 内部类实体

Module:

    // 约束传入参数结构实体
    // 内部类作为resultMap需使用$符号:ModuleTreeDTO$Module
    @Data
    public static class Module implements Serializable, Comparable {
        private static final long serialVersionUID = 666L;
        private Integer id;
        private Integer parentId;
        private String name;
        private Boolean hasChildren;
        private String index;
        private Integer displayOrder;

        @Override
        public int compareTo(Module o) {
            return displayOrder.compareTo(o.displayOrder);
        }
    }

Node:

    // 菜单树节点
    @Data
    private static class Node implements Serializable {
        private static final long serialVersionUID = 777L;
        private String name;
        private Boolean hasChildren;
        private String index;
        private final List children = new ArrayList<>();

        public void addChildren(Node node) {
            children.add(node);
        }

        public Node(String name, Boolean hasChildren, String index) {
            this.name = name;
            this.hasChildren = hasChildren;
            this.index = index;
        }
    }

StackEnpty:

    // 栈数据实体
    @Data
    private static class StackEntry implements Serializable {
        private static final long serialVersionUID = 888L;
        private Integer id;
        private Integer parentId;
        private Node node;

        public StackEntry(Integer id, Integer parentId, Node node) {
            this.id = id;
            this.parentId = parentId;
            this.node = node;
        }
    }

2. 流程实施

按前面的流程图,代码的实施照着流程走向就能实现了,没什么难度,这里就不贴了。

(需要评论区T)

单元测试:

    @Test
    public void testConvert() {
        ModuleTreeDTO moduleTreeDTO = new ModuleTreeDTO();
        List list = new ArrayList<>();
        ModuleTreeDTO.Module m = new ModuleTreeDTO.Module();
        m.setId(1);
        m.setParentId(null);
        m.setIndex("111");
        m.setHasChildren(true);
        m.setName("111");
        m.setDisplayOrder(1);
        list.add(m);
        ModuleTreeDTO.Module m1 = new ModuleTreeDTO.Module();
        m1.setId(101);
        m1.setParentId(1);
        m1.setIndex("11101");
        m1.setHasChildren(true);
        m1.setName("11101");
        m1.setDisplayOrder(2);
        list.add(m1);
        ModuleTreeDTO.Module m2 = new ModuleTreeDTO.Module();
        m2.setId(10101);
        m2.setParentId(101);
        m2.setIndex("1110101");
        m2.setHasChildren(false);
        m2.setName("1110101");
        m2.setDisplayOrder(3);
        list.add(m2);
        ModuleTreeDTO.Module m3 = new ModuleTreeDTO.Module();
        m3.setId(10102);
        m3.setParentId(101);
        m3.setIndex("1110102");
        m3.setHasChildren(false);
        m3.setName("1110102");
        m3.setDisplayOrder(4);
        list.add(m3);
        ModuleTreeDTO.Module m4 = new ModuleTreeDTO.Module();
        m4.setId(102);
        m4.setParentId(1);
        m4.setIndex("11102");
        m4.setHasChildren(false);
        m4.setName("11102");
        m4.setDisplayOrder(5);
        list.add(m4);

        moduleTreeDTO.convert(list);
        System.out.println(moduleTreeDTO);
    }

四、小结

1. 比较复杂的逻辑在流程图画出来后就是很简单了。

2. 总结归纳总有新的收获,比如菜单结构与树的先、后序遍历的关系是在完成菜单的树形转化代码实现后,现在总结归纳得出的。

你可能感兴趣的:(工具,数据结构与算法,java,数据结构)