目录
前言
一、结构分析
1. 菜单显示结构
2. 逻辑分层
3. 抽象节点
二、概念转化
1. 逻辑栈代替递归栈
2. 结构成员
3.结构流程
4. 栈顶调整
三、代码实施
1. 内部类实体
2. 流程实施
四、小结
菜单保存在数据库中可以满足动态维护的需要。在查询菜单的时候,经常会使用到connect by在数据库中查询出一个树形的菜单数据结构,本质是菜单树的先序遍历List。在返回给前端数据前,会将List处理成一个树形结构(本质是List、Map的多级嵌套)。
后端在处理菜单树形结构的时候,通常会使用递归的方式。在递归调用的时候,需要向下传递完整的数据List和父级菜单id,再回传处理结果List
在此,对菜单造树方式进行进一步优化,使用逻辑栈代替递归栈,对按先序排列的List进行一次性遍历的同时,借助逻辑栈的辅助(暂存父节点),按后序,从左到右,从下到上将子菜单节点与父节点关联起来,遍历结束即可得到所需的结构数据。
菜单显示结构大概如上图(手码,略丑),前端接收到的数据格式大概如下:
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"
}
]
}
],
按上下级关系分层:
真个菜单无法抽象成一个固定参数结构的对象,但各级菜单项拥有一样的数据结构,可以将每个菜单项抽象成菜单节点对象,上下级关系通过引用指向关联。最终菜单对象只需要存储一级菜单引用集合。
根据前端提供的所需数据结构,每个菜单项都包含name、index参数,hasChildren、children参数只在非底层节点包含,可以看做hasChildren=false、children=[]在底层节点被隐藏了。至此可以确定菜单节点对象的参数结构。
旧递归方式如下:
public List
当当前菜单拥有子菜单的时候,当前菜单操作挂起,递归等待孩子节点返回。其实就是儿子找爸爸,爸爸等孩子的过程。
递归全遍历List的方式对数据的顺序要求相对较低,因为每次递归数据普及度高,只要求同级菜单按顺序排列即可。但想要单次遍历完成操作,则需要数据严格按照前序遍历顺序排列,所以一般菜单表结构会引入一个display order栏位,其记录的就是菜单数据项的前序遍历顺序。可以直观地看出,菜单的展示顺序跟前序遍历顺序是一样的:
根据前序遍历顺序先根后左再右的顺序特征,只要存在孩子节点,遍历会一直往树的左边走。参照递归,遍历路径上有孩子的节点会暂时入栈等待,没有孩子的节点直接处理关联到栈顶父节点。可见造树的过程是一个树的后序遍历的过程,从整颗树最左节点到同父最右节点逐个归属到父节点,遍历处理完孩子节点后再进行父节点的后续操作。
清楚结构后,使用LinkedList作为一个轻量级的逻辑栈,链表尾部元素作为栈顶,用来暂存有孩子节点的父节点,等其孩子节点关联完父节点再将该父节点出栈处理。在顶端节点(一级菜单)处理完后,将顶端节点并入到菜单结果集中。
问题引申:
(1)如何确定子节点已经全部遍历完毕
(2)前序遍历与后序遍历不同步的问题:主体遍历到达右子节点结束前序遍历时,造树遍历也只遍历到右子树,未到达父节点完成后序遍历。
至此,可以确定需要的菜单类结构成员有:约束传入数据结构的对象、菜单节点对象、存放父节点的逻辑栈、存储顶端节点的集合。
针对1(1)的问题,这里的解决方案是同时在栈中保存存储节点的Id和parentId,在当前遍历节点parentId不等于栈顶节点的Id的时候,代表栈顶节点的子节点已全部遍历完毕。故需要增加一个栈数据对象。
菜单类结构成员:
Module之所以实现实现Comparable接口是为了进入转化树的流程前,将源数据再进行一次按displayOrder的排序,确保数据顺序。
首先明确:
(1)当前节点的父节点Id(parentId)为null代表当前是一级菜单节点;
(2)当前节点parentId不等于栈顶节点的Id(rootId)时,代表栈顶节点的子节点已全部遍历完毕;
(3)栈顶调整函数是在完成栈顶节点的子节点遍历后,继续出栈做操作,完成未完成的造树后序遍历,直到栈顶节点为当前节点的父节点。
以下流程图使用伪代码表示(思路按流程走,就不废话了):
说明:
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?:只做一个循环遍历的表示,遍历方式有多种。
栈顶调整函数是1(2)问题的一个解决,是对栈中节点的一个遍历,以完成后续的后序遍历。流程伪代码示意与3类似。
直接上流程:
说明:
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有误或其他未预知格式问题,抛出异常
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;
}
}
按前面的流程图,代码的实施照着流程走向就能实现了,没什么难度,这里就不贴了。
(需要评论区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. 总结归纳总有新的收获,比如菜单结构与树的先、后序遍历的关系是在完成菜单的树形转化代码实现后,现在总结归纳得出的。