传统的多层级结构取数时,我们多数使用递归解决问题,但递归取数有经验的人都知道有几个坏处:一是如果层级过多会导致取数时间非常久,二是资源一直得不到释放,会占用大量内存。本文旨在给大家分享一下通过引用传递实现获取层级结构数据。
首先我们查看一下二者在不同层级时对应消耗时间
场景一:三层数据
场景:每层级最多节点:10,循环次数:100。细说就是这是一个三层的数据结构,第一层1个数,第二层的节点最少有1个最多有10个,依次类推,第二层每个节点又会创建1-10个子节点。然后循环100次构建这种三层结构的树,并分别用两种方式针对这产生的一百个树进行取数,二者取第一层级树(取树形结构的所有数据)的时间图如下:
结果解读:上图中横坐标为次数,第一次,第二次...;纵坐标为每次消耗的时间,目前单位为毫秒数。从上图时间分析来看当树结构为三层时,两种方式时间相差不大,所以此时递归和引用两种方式可以随意切换。
场景二:四层数据
场景:每层级最多节点:10,循环次数:100。细说就是这是一个四层的数据结构,第一层1个数,第二层的节点最少有1个最多有10个,依次类推。。。。。。然后循环100次构建这种四层结构的树,并分别用两种方式针对这产生的一百个树进行取数,二者取第一层级树(取树形结构的所有数据)的时间图如下:
结果解读:上图中横坐标为次数,第一次,第二次...;纵坐标为每次消耗的时间,目前单位为毫秒数。从上图时间分析来看当树结构为四层时,部分场景(即数据量稍微多点)的时候我们可以看到递归取树所消耗的时间要明显高于引用,所以此时基本已经可以考虑使用引用取树的方式,但由于时间基本还是几毫秒的差别,所以仍然可以选择递归。
场景三:五层数据
场景和之前一样,增加了四层到五层,此时100次取5层树结构所消耗时间如下:
结果解读:从上图时间分析来看当树结构为五层时,我们基本可以看到递归取树所消耗的时间要明显高于引用,基本都是十几毫秒的差距了,此时可以选择使用引用方式获取。
场景三:六层数据
场景和之前一样,增加了四层到五层,此时100次取6层树结构所消耗时间如下:
结果解读:从上图时间分析来看当树结构为六层时,递归方式取树消耗的时间已经远远大于引用方式取树的时间,此时毫无疑问肯定是选择使用引用方式取树的。
场景四:七层数据
结果解读:当结果为7层时可以很明显看到递归方式消耗的时间已经不用多说了。
结论
1.消耗时间
由上图可以看出来,当树结构为三层或四层时,引用方式取树和递归方式取树所消耗的时间差别不大,但当层级为五层及往上时,可以很明显的发现引用方式取树所消耗的时间要低于甚至远远低于递归方式取树所消耗的时间。
2.消耗内存
内存这里我不做过多的比较,由于递归的机制决定了递归方式会堆积大量变量得不到释放,所以造成的内存消耗肯定是要大于引用方式的。引用方式不消耗内存或者说明显没有递归方式消耗内存,我们在后续讲解如何使用引用方式取树会说明。
如何使用引用方式取树
1.场景
结合工作中实际遇到的场景来跟大家分享一下,如何使用引用方式取树。具体使用案例图如下:
2.案例分析
针对上述功能,我们表结构设计为每个层级都为模块表的一条记录,每个子节点会记录父级节点的id,即parent_id。此时每个层级的模块最终会落库为一条一条的数据,此时我们需要考虑如何将拉平的数据获取出来,组成树结构输出,以及如何获取到树结构的数据,拉平结构入库。另外由于模块内部有针对某一个节点新增模块的,所以我们需要考虑提供获取某个指定节点下的树结构。
3.代码实现
定义数据对应实体类:
public class Node {
private String id; // 当前节点id
private String parentId; // 父级节点id
private List<Node> children; // 子级节点列表
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
public List<Node> getChildren() {
return children;
}
public void setChildren(List<Node> children) {
this.children = children;
}
}
类定义好之后,我们从数据库查询可获取到一个Node的List,此时针对这种拉平的节点列表,我们可以来组装树结构。
代码结构如下:
// 存放所有节点的对象map
Map<Integer, Node> allNodeMap = new HashMap<>();
// key为Node的id,Value为当前节点
for (Node node : allNodeList) {
allNodeMap.put(node.getId(), node);
}
// 遍历所有节点
for (Node node : allNodeList) {
if (StringUtils.isEmpty(node.getParentId())){
continue;
}
// 取出父级节点对应的节点,取出父级节点存放子级的列表,将当前节点存入
Node itemNode = allNodeMap.get(node.getParentId());
if (itemNode.getChild() == null) {
List<Node> nodes = new ArrayList<>();
nodes.add(info);
itemNode.setChild(infos);
} else {
itemNode.getChild().add(info);
}
}
如上最终得到的allNodeMap中每个节点都是已经存储好子级引用,所以想要取某个节点对应的树,就直接找到该节点id,直接取出其中对应的Node对象,即为带着层级的数据。
4.实现原理
我们之所以称此方式为引用取树,根本逻辑是将每两个存在子父级关系的节点通过引用建立关系,搭上线。所以当流程走完之后,就可以得到一个完整的引用关系。此方式相比较递归传递并没有新创建变量,所以在内存占用量上肯定优于递归取树的方式。
5.根据树形结构拉平数据
/**
*
* 这里方便大家看的清晰,就没有做同样代码的逻辑抽取
*/
private List<Node> tileNodeList(List<Node> nodeList) {
List<Node> copyNodeList = new ArrayList<>();
for (Node node : nodeList) {
Node copy = new Node();
BeanUtil.copyProperties(node,copy);
copy.setChild(null);
copyNodeList.add(copy);
}
for (Node node : nodeList) {
if (!CollectionUtils.isEmpty(node.getChild())){
itemChildDeal(node.getChild(),copyNodeList);
}
}
return copyNodeList;
}
private void itemChildDeal(List<Node> child,List<Node> copyNodeList){
for (Node node : child) {
Node copy = new Node();
BeanUtil.copyProperties(node,copy);
copy.setChild(null);
copyNodeList.add(copy);
}
// 分开循环,避免在上层循环中递归造成内存不释放
for (Node node : child) {
if (!CollectionUtils.isEmpty(node.getChild())){
itemChildDeal(node.getChild(),copyNodeList);
}
}
}
其中细节点是递归循环的时候,单独将子级的循环外置,可以释放掉上层级的变量。