一、引言
最近公司新进了不少新人,包括一些来自阿里、网易等大型企业的资深工程师。我们组的一位新同事是阿里来的专家,我在CR(Code Review, 简称CR)时看到了他编写的一个关于树操作的工具类,对其设计和实现深感佩服。为了进一步理解和学习,我对这个工具类进行了深入分析和整理,现在在本文中与大家分享。
二、树形结构介绍
2.1 简单的二叉树
首页简单简介一下树形数据结构,树形数据结构是一种层级化的数据组织方式,广泛用于表示具有层次关系的数据。由于其层级化组织特点,树形数据结构能够高效支持多种查找、插入和删除操作,因此在计算机科学和实际应用中得到了广泛应用。下面是一个简单的二叉树示例:
▲二叉树及遍历算法
2.2 树的应用场景
树形数据结构的应用场景是通过ID关联上下级关系的对象,然后将这些对象组织成一棵树。主要有以下常用应用场景:
- 部门通讯录:通讯录中可以通过树形结构展示不同部门及其上下级关系,便于用户快速找到联系人
- 系统菜单: 系统中的菜单通常是分层的,通过树形结构可以方便地展示和管理各级菜单项
- 地址选择器:地理地址通常有多级关系,如省、市、县,通过树形结构可以方便用户选择具体的地址
- 文件夹目录: 文件系统中的文件夹和文件可以通过树形结构来组织和展示,便于用户进行文件操作
- 产品多级分类: 通过树形结构可以直观地展示和管理各级分类
- 评论回复: 通过树形结构可以展示帖子的回复关系,便于查看讨论的脉络
树形数据结构的应用场景通常是分层的,通过树形结构可以展示和管理各级流程节点及其关系。 这些场景中,树形结构的应用可以显著提升数据的组织和展示效率,帮助用户更直观地理解和操作系统。
▲:树形结构在电商分类中的应用
三、JAVA中的树形数据结构
3.1 JAVA树形结构对象定义
在JAVA中树形结构是通过对象嵌套方式来定义的,如MenuVo对象中有一个子对象subMenus:
3.2 JSON数据格式中的树形结构
JSON数据天然就是树形结果,如以下展示一个简单的JSON树形结构:
3.3 树形数据结构的储存
像文档型数据库MongDB、ElasticSearch可以直接存储JSON这种树形数据,而像Mysql这种关系型数据库不太适合直接存储具有上下级关系的树形数据,大都是按行存储然后通过id、pid之间关联上下级关系,这就导致我们需要经常将Mysql中的关系型数据转成JSON这种数据结构,因而TreeUtil这类数据转换工具类就起诞生了。
▲:数据库中存储的城市数据结构
四、TreeUtil代码分析
4.1 makeTree()构建树
直接看这神一样的方法makeTree():
是不是完全看不懂?像看天书一样?makeTree方法为了通用使用了泛型+函数式编程+递归,正常人一眼根本看不这是在干什么的,我们先不用管这个makeTree合成树的代码原理,先直接看如何使用:
我们结合这个简单的合成菜单树看一下这个makeTree()方法参数是如何使用的:
- 第1个参数List list,为我们需要合成树的List,如上面Demo中的menuList
- 第2个参数Predicate rootCheck,判断为根节点的条件,如上面Demo中pId==-1就是根节点
- 第3个参数parentCheck 判断为父节点条件,如上面Demo中 id==pId
- 第4个参数setSubChildren,设置下级数据方法,如上面Demo中: Menu::setSubMenus
有了上面这4个参数,只要是合成树场景,这个TreeUtil.makeTree()都可以适用,比如我们要合成一个部门树:
groupId是部门ID, 根部门的条件是parentGroupId=null, 那么调用合成树的方法为:
是不是很优雅?很通用?完全不需要实现什么接口、定义什么TreeNode、增加什么TreeConfig,静态方法直接调用就搞定。 一个字:绝!
五、神方法拆解
5.1 去掉泛型和函数接口
第一步我们可以把泛型和函数接口去掉,再看一下一个如何使用递归合成树:
调用方法:
通过上面的两个方法可以合成树的基本逻辑,主要分为三步
- 找到所有根节点
- 遍历所有根节点设置子节点
- 遍历allDate查询子节点
5.2 使用函数优化
看懂上面的代码后,我们再给加上函数式接口:
结合前面的方式再来看这个函数式接口是不是简单多了,只是写法上函数化了而已。使用函数优化的整体结构和最终的方法有点像了,最后再使用泛型优化就成了最终版本。从这个例子来看代码还是要不断优化的,大神可以直接写出神一样的代码,小弟一步步优化,一点点进步也是能写出大神一样的代码的。
六、其它操作Tree方法
6.1 遍历Tree
学习过二叉树都知道遍历二叉树有先序、中序、后序、层序,如果这些不清楚的可以先去学习一下,针对Tree的遍历这里提供了三个方法: 先序forPreOrder(),后序forPostOrder(),层序forLevelOrder():
我们看测试方法:
通过这个Demo我们解释一下遍历中的几个参数:
- tree 需要遍历的树,就是makeTree()合成的对象
- Consumer consumer 遍历后对单个元素的处理方法,如:x-> System.out.println(x)、 postOrder.append(x.getId().toString())
- Function<E, List> getSubChildren,获取下级数据方法,如Menu::getSubMenus
有了这三个方法遍历Tree是不是和遍历List一样简单方便了?二个字:绝了!!
6.2 flat打平树
我们可以将一个List使用markTree()构建成树,就可以使用flat()将树还原成List
使用方法:
flat()参数解释:
- tree 需要打平的树,就是makeTree()合成的对象
- Function<E, List> getSubChildren,获取下级数据方法,如Menu::getSubMenus
- Consumer setSubChildren,设置下级数据方法,如: x->x.setSubMenus(null)
6.3 sort()排查
我们知道针对List,可以使用list.sort()直接排序,那么针对树,就可以调用sort()方法直接对树中所有子节点直接排序:
比如MenuVo有一个rank值,表明排序权重
如查我们想按rank正序:
如果我们想按rank倒序:
sort参数解释:
- tree 需要排序的树,就是makeTree()合成的对象
- Comparator<? super E> comparator 排序规则Comparator,如:Comparator.comparing(MenuVo::getRank)按Rank正序 ,(x,y)->y.getRank().compareTo(x.getRank()),按Rank倒序
- Function<E, List> getChildren 获取下级数据方法,如:MenuVo::getSubMenus
这个给树排序是不是和对List排序一样的简单:三个字:太绝了!!!
七、总结
看完这位大神编写的TreeUtil工具类后,我深感佩服,其设计与实现真是令人叹为观止。该工具类不仅优雅且高效,使得以往需要递归处理的树形结构操作变得更加简洁和便捷。未来处理树形数据时,只需直接使用该工具类即可,无需再编写复杂的递归代码。
最后附完成代码方便CV工程师,还不赶快点赞、关注、收藏