树形结构!别再用递归实现了这才是最佳的方案;更快更强更好用

Java
443
0
0
2023-06-09

不管你是做前端还是后端的开发,那我相信树形结构的需求一定有遇到过,特别是管理平台类型的项目,一般都会有一个树形结构的菜单栏,再比如说, 公司组织架构 层级关系 归属关系 等等需求,本质上都是树形结构的一种体现;

遇到这种需求,最常见也最容易想到的设计思路就是: 父子关系的方式 ,子项通过一个字段来保存他的父ID,然后通过递归的方式得到层级关系;

前几天,技术交流群里面有小伙伴在问,实际的业务中,树形结构的数据太多、层级还深,查询过程一顿递归之后,性能表现得比较差,问有没有什么好的设计思路,让查询、统计更加的便捷高效;

今天就给大家介绍一种新的树形结构设计方案: 改进后的先序树方式 ,在查询、统计方面的优势,要远大于父子关系的递归设计方案;

本文就来详细的讲解并对比一下两个方案的实现方式以及优缺点。

文章目录:

对于树形结构的需求,不管是采用什么方式,要处理的问题都是差不多的,下面先列举一下树形结构常见的问题点有哪些:

  • 节点的增删改
  • 是否存在子节点( 叶子 节点)
  • 查询出所有的子节点
  • 查询所有的节点
  • 查询所有的子孙节点
  • 父节点查询
  • 祖先节点查询
  • 统计所有子孙部门的数量

针对上面的这些问题,就以一个简单的公司组织架构示例,一起来看看,两种方案都是如何实现和解决的?

本文所有的示例都是采用的 MySQL + Java 实现,核心思路都类似,实际使用,可根据语言特性以及自己习惯的方式调整即可。

1父子关系方案

父子关系,顾名思义,就是当前节点只关注自己的父节点是谁,并将其保存起来即可,查询我的子节点有哪些,只需要全局找到所有父ID是和我的ID一致的项;

如下图所示:

方案特点

  • 优点
  • 方案简单易懂
  • 数据结构简单清晰
  • 层级直观、鲜明
  • 易维护层级关系只需要关注自己的父ID,所以在添加、修改的时候,一旦关系发生变化,调整对应的父ID即可。
  • 缺点
  • 查找麻烦、统计麻烦根据当前节点的数据,只能获取到子节点的数据,一旦查询、统计超出父子范围,就只能通过递归逐层查找了;

示例

根据上面的图示示例,与其对应的表结构如下:

IDdep_name(部门名称)level(层级)parent_id(父ID) 1董事会10 2总经理21 3董事会秘书21 4产品部32 5行政总监32 6设计部44 7技术部44 8财务部45 9行政部45 10客户端57 11服务端57

SQL 脚本:

 DROP TABLE IF EXISTS `department_info`;
CREATE TABLE `department_info`  (
  `id` int() NOT NULL,
  `dep_name` varchar() CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
  `level` int() NULL DEFAULT NULL,
  `parent_id` int() NULL DEFAULT NULL COMMENT '父ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utfmb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `department_info` VALUES (, '董事会', 1, 0);
INSERT INTO `department_info` VALUES (, '总经理', 2, 1);
INSERT INTO `department_info` VALUES (, '董事会秘书', 2, 1);
INSERT INTO `department_info` VALUES (, '产品部', 3, 2);
INSERT INTO `department_info` VALUES (, '行政总监', 3, 2);
INSERT INTO `department_info` VALUES (, '设计部', 4, 4);
INSERT INTO `department_info` VALUES (, '技术部', 4, 4);
INSERT INTO `department_info` VALUES (, '财务部', 4, 5);
INSERT INTO `department_info` VALUES (, '行政部', 4, 5);
INSERT INTO `department_info` VALUES (, '客户端', 5, 7);
INSERT INTO `department_info` VALUES (, '服务端', 5, 7); 

函数的创建

由于父子节点的查询,需要依赖递归,为了查询方便,这里创建两个函数:

  • 递归查询子孙节点ID的函数 DROP FUNCTION IF EXISTS queryChildrenDepInfo;
  • DELIMITER ;;
  • CREATE FUNCTION queryChildrenDepInfo(dep_id INT )
  • RETURNS VARCHAR ( 4000 )
  • BEGIN
  • DECLARE sTemp VARCHAR ( 4000 );
  • DECLARE sTempChd VARCHAR ( 4000 );
  • SET sTemp= ‘$’ ;
  • SET sTempChd = CAST (dep_id AS CHAR );
  • WHILE sTempChd IS NOT NULL DO
  • SET sTemp= CONCAT (sTemp, ‘,’ ,sTempChd);
  • SELECT GROUP_CONCAT ( id ) INTO sTempChd FROM department_info WHERE FIND_IN_SET(parent_id,sTempChd)> 0 ;
  • END WHILE ;
  • RETURN sTemp;
  • END
  • ;;
  • DELIMITER ;
  • 测试:查询技术部下的所有重要节点? SELECT queryChildrenDepInfo( 4 );
  • SELECT * FROM department_info WHERE FIND_IN_SET( id ,queryChildrenDepInfo( 4 ));
  • 递归查询祖先节点ID的函数 DROP FUNCTION IF EXISTS queryParentDepInfo;
  • DELIMITER;;
  • CREATE FUNCTION queryParentDepInfo(dep_id INT )
  • RETURNS VARCHAR ( 4000 )
  • BEGIN
  • DECLARE sTemp VARCHAR ( 4000 );
  • DECLARE sTempChd VARCHAR ( 4000 );
  • SET sTemp= ‘$’ ;
  • SET sTempChd = CAST (dep_id AS CHAR );
  • SET sTemp = CONCAT (sTemp, ‘,’ ,sTempChd);
  • SELECT parent_id INTO sTempChd FROM department_info WHERE id = sTempChd;
  • WHILE sTempChd <> 0 DO
  • SET sTemp = CONCAT (sTemp, ‘,’ ,sTempChd);
  • SELECT parent_id INTO sTempChd FROM department_info WHERE id = sTempChd;
  • END WHILE ;
  • RETURN sTemp;
  • END
  • ;;
  • DELIMITER ;
  • 测试:查询技术部所有的祖先节点? SELECT queryParentDepInfo( 7 );
  • SELECT * FROM department_info WHERE FIND_IN_SET( id ,queryParentDepInfo( 7 ));

节点的增删改

  • 增加节点 比如要在技术部下添加一个测试部门 INSERT INTO department_info( `id` , `dep_name` , `level` , `parent_id` ) VALUES ( 12 , ‘测试部’ , 5 , 7 );
  • 修改节点 比如:将测试部(ID = 12)提出来,放到产品部(ID = 4)下;就只需要将测试部对应的父节点ID修改为4即可 SET @ id = 12 ;
  • SET @pid = 4 ;
  • UPDATE department_info SET `parent_id` = @pid WHERE `id` = @ id ;
  • 删除节点 删除相比于添加修改,情况就会特殊一些,如果删除的节点存在子节点,意味着子节点也需要同步删除掉;因此这里就需要用到上面创建的递归查询子孙节点ID的函数(queryChildrenDepInfo)比如:删除技术部门; DELETE FROM department_info WHERE FIND_IN_SET( id ,queryChildrenDepInfo( 7 ));

是否存在子节点(叶子节点)

在该方案下,要想判断是否是叶子节点,有两种实现方式:

  • 统计当前节点以及子孙节点的数量递归查询所有子节点的ID,并统计数量,由于函数查询包含了节点自身,所以这里使用了 COUNT(*)-1 来计算子节点的数量,如果等于0就是叶子节点,大于0说明不是叶子节点; — 查看设计部(ID=6)是不是叶子节点
  • SET @ id = 6 ;
  • — 由于数量包含了自身,由于统计的是子节点的数量,所以这里需要-1将自己去掉
  • SELECT COUNT (*) -1 FROM department_info WHERE FIND_IN_SET( id ,queryChildrenDepInfo(@ id ));
  • 添加叶子节点的标记在表中添加一个isLeaf字段,当节点增删改操作的时候,用这个字段来标记一下当前是否是叶子节点

查询出所有的下级部门(子节点)

查询所有的下级部门,此时就需要借助当前节点的id和level字段

例:查询产品部(id = 4,level = 3)的子节点

 SET @id =;
SET @lv =;
SELECT * from department_info where parent_id = @id AND `level` = @lv +; 

查询所有的下下级部门(孙节点)

实际业务场景下,这种需求很少,这里主要用来演示可操作性,不排除特殊的场合下用得上;

查询孙节点相比与子节点就麻烦很多了,因为当前节点和孙节点是没有任何数据上的关联,因此需要借助子节点才能找到孙节点,因此这里又必须得用上递归查询子孙节点ID的函数了;

例:查询产品部(id = 4,level = 3)的孙节点

 -- 查询孙节点
SET @id =;
SET @lv =;
SELECT * FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(@id)) AND `level` = @lv +; 

查询所有的下属部门

查询下属部门就和查询下下级部门(孙节点)类似,只是不用校验层级即可;

例:查询产品部下所有的下属部门?

 SET @id =;
SELECT * FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(@id)); 

查询隶属部门

也就是查询节点;在该方案下,节点中已经保存了父节点的ID,通过ID就能直接获取到父节点

查询所有上级部门

由于当前节点只保存了父级节点ID,更上一级的信息只能通过递归逐级获取;

例:查询技术部(id = 7)的所有上级部门;

 SET @id =;
SELECT * FROM department_info WHERE FIND_IN_SET(id,queryParentDepInfo(@id)); 

统计所有下属部门的数量

和查询是否是叶子节点的方式一样,只是对得到的数据解读的方式不同而已;

例:统计技术部的下属部门数量?

 SET @id =;
SELECT COUNT(*)- FROM department_info WHERE FIND_IN_SET(id,queryChildrenDepInfo(@id)); 

2改进后的先序树方案

从从上述的父子关系方案可以看出,大部分的操作,都需要递归查询出所有的子孙节点后才行,如果出现文章开始说的,层级多,深度深的话,递归的过程,就会大大影响查询、统计的性能;

下面就来介绍一下改进后的先序树的树形结构方案,节点不再保存父节点的ID,而是 为每个节点增加左值和右值

如下图示:

对应的数据如下:

iddep_name(部门名称)lt(左值)rt(右值)lv(层级) 1董事会1221 2总经理2192 3董事会秘书20212 4产品部3123 5行政总监13183 6设计部454 7技术部6114 8财务部14154 9行政部16174 10客户端785 11服务端9105

SQL语句:

 DROP TABLE IF EXISTS `department_info`;
CREATE TABLE `department_info`  (
  `id` int() NOT NULL AUTO_INCREMENT,
  `dep_name` varchar() CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
  `lt` int() NOT NULL COMMENT '节点左数',
  `rt` int() NOT NULL COMMENT '节点右数',
  `lv` int() NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `department_info` VALUES (1, '董事会', 1, 22, 1);
INSERT INTO `department_info` VALUES (2, '总经理', 2, 19, 2);
INSERT INTO `department_info` VALUES (3, '董事会秘书', 20, 21, 2);
INSERT INTO `department_info` VALUES (4, '产品部', 3, 12, 3);
INSERT INTO `department_info` VALUES (5, '行政总监', 13, 18, 3);
INSERT INTO `department_info` VALUES (6, '设计部', 4, 5, 4);
INSERT INTO `department_info` VALUES (7, '技术部', 6, 11, 4);
INSERT INTO `department_info` VALUES (8, '财务部', 14, 15, 4);
INSERT INTO `department_info` VALUES (9, '行政部', 16, 17, 4);
INSERT INTO `department_info` VALUES (10, '客户端', 7, 8, 5);
INSERT INTO `department_info` VALUES (11, '服务端', 9, 10, 5); 

方案特点

  • 优点
  • 查询汇总简单高效
  • 无需递归查询,性能高
  • 缺点
  • 结构相对复杂,数据层面理解难度较高
  • 不易维护以为左右值的存在,会直接影响到后续的节点,因此,当前节点增删改时,都会对后续的节点产业影响;

示例

节点的增删改

  • 新增 如下图:在技术部下新增一个测试部,新增节点对应的左右值分别为11、12;过程分析: 第一步 ,所有比11(新增节点的左数)大的左数全部+2(紫色部分) 第二步 ,所有比12(新增节点的右数)大的右数全部+2(橙色部分) 第三步 ,添加左右数分别为11、12的部门节点 由于这里涉及到多个步骤,我们需要保证数据库操作的原子性,因此需要做事务操作,这里为了方便,创建一个存储过程;存储过程并不式必须的,同样也可以以代码的形式来保证事务操作;只是使用存储过程,可靠性会高一些; 添加节点存储过程 DROP PROCEDURE IF EXISTS insertDepartment;
  • CREATE PROCEDURE insertDepartment( IN lt_i INT , IN rt_i INT , IN lv_i INT , IN dn VARCHAR ( 256 ))
  • BEGIN
  • DECLARE d_error INTEGER DEFAULT 0 ;
  • DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET d_error= -2 ; — 异常时设置为1
  • DECLARE CONTINUE HANDLER FOR NOT FOUND SET d_error = -3 ; — 如果表中没有下一条数据则置为2
  • START TRANSACTION ;
  • — 将所有左值大于新增左值的部门全部+2
  • UPDATE department_info2 SET lt=lt+ 2 WHERE lt > lt_i;
  • — 将所有右值大于新增右值的部门全部+2
  • UPDATE department_info2 SET rt=rt+ 2 WHERE rt >= lt_i;
  • — 新增部门
  • INSERT INTO department_info2(dep_name,lt,rt,lv) VALUES (dn,lt_i,rt_i,lv_i);
  • SET d_error= ROW_COUNT ();
  • IF d_error != 1 THEN
  • ROLLBACK ; — 结果不等于1 的时候,说明新增失败,回滚操作
  • ELSE
  • COMMIT ; — 等于1的时候,说明添加成功,提交
  • END IF ;
  • select d_error;
  • END
  • 输入参数 lt_i :新增部门的左值 rt_i :新增部门的右值 lv_i :行政部门的层级 dn :部门名称 如上图的示例,就可以调用新增的存储过程添加: call insertDepartment( 11 , 12 , 5 , “测试部” );
  • 修改 普通的节点信息修改,这里就不多多说了,很简单; 节点移动是此方案下,最复杂的修改操作 ,因为整个过程涉及到位置的变化,层级的变化等多个维度的调整,而且还得保证事务操作; 例:我们要将技术部(id = 4)交由总经理(id = 2)直接负责,也就是移动到总经理之下;过程分析: 第一步 ,计算技术部的左右数差值 第二步 ,计算与移动后的上级节点之间的差值 第三步 ,确定是左移还是右移 第四步 ,将本节点与目标节点之间的差值减去(左移)/加上(右移) 第五步 ,将移动的节点以及子孙的节点与父节点之间的差值减去(左移)/加上(右移) 第六步 ,调整层级 整个过程如下图所示,稍微有点点复杂,可以结合图示以及存储过程的代码,仔细的理解一下:为了方便操作,避免出错,这里同样以存储过程的方式来实现核心逻辑,降低出错风险: DROP PROCEDURE IF EXISTS moveDepartment;
  • CREATE PROCEDURE moveDepartment( IN fid INT , IN tid INT )
  • BEGIN
  • DECLARE d_error INTEGER DEFAULT 0 ;
  • DECLARE num INTEGER DEFAULT 0 ; — 删除节点左右值之间的差值
  • DECLARE mnum INTEGER DEFAULT 0 ; — 移动阶段与上级节点之间的差值
  • DECLARE ids VARCHAR ( 1000 ); — 保存所有正在移动的id集合,保证多个节点移动时也能正常
  • DECLARE blt INT ; — 需要移动节点的左数
  • DECLARE brt INT ; — 需要移动节点的右数
  • DECLARE blv INT ; — 需要移动节点的层级
  • DECLARE tlt INT ; — 目标节点的左数
  • DECLARE trt INT ; — 目标节点的右数
  • DECLARE tlv INT ; — 目标节点的层级
  • DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET d_error= -2 ; — 异常时设置为1
  • DECLARE CONTINUE HANDLER FOR NOT FOUND SET d_error = -3 ; — 如果表中没有下一条数据则置为2
  • START TRANSACTION ;
  • — 查询待移动的节点的左右数以及层级
  • SELECT lt,rt,lv INTO blt, brt,blv FROM department_info2 WHERE id = fid;
  • — 查询目标节点的左右数以及层级
  • SELECT lt,rt,lv INTO tlt, trt,tlv FROM department_info2 WHERE id = tid;
  • — 查询所有要一定的节点,当前节点以及其子孙节点
  • SELECT GROUP_CONCAT ( id ) INTO ids FROM department_info2 WHERE lt>=blt AND rt <=brt;
  • IF tlt > blt AND trt < brt THEN
  • — 将自己移动到自己的下属节点 暂时不允许 如果要如此操作,需要将下级调整为平级 再移动
  • SET d_error = -4 ;
  • ELSEIF blt > tlt AND brt < trt AND blv = tlv + 1 THEN
  • — 说明本身就已经是目标节点在子节点了,不需要处理,直接成功
  • SET d_error = 0 ;
  • ELSE
  • — 计算移动节点与上级节点之间的差值
  • SET mnum = trt – brt -1 ;
  • — 计算当前节点及其子节点的差值
  • SET num = brt – blt + 1 ;
  • — 首先将移动前的节点整个元素表中移除掉
  • IF trt > brt THEN
  • — 左往右移动
  • UPDATE department_info2 SET lt=lt- num WHERE lt > brt AND lt < trt;
  • UPDATE department_info2 SET rt=rt- num WHERE rt > brt AND rt < trt;
  • ELSE
  • — 从右往左移动 将系数全部变为负值,-负数就等于+正数
  • SET mnum = -mnum;
  • SET num = – num ;
  • UPDATE department_info2 SET lt=lt- num WHERE lt >= trt AND lt < blt;
  • UPDATE department_info2 SET rt=rt- num WHERE rt >= trt AND rt < blt;
  • END IF ;
  • — 调整移动的节点以及下属节点
  • UPDATE department_info2 SET lt=lt+mnum,rt=rt+mnum,lv = lv – (blv – tlv -1 ) WHERE FIND_IN_SET( id ,ids);
  • SET d_error= ROW_COUNT ();
  • IF d_error <= 0 THEN
  • ROLLBACK ;
  • ELSE
  • COMMIT ;
  • END IF ;
  • END IF ;
  • select d_error;
  • END
  • 输入参数 fid :移动的节点id tid :目标节点id 测试: CALL moveDepartment( 7 , 2 )
  • 删除 删除的过程正好与新增相反,在删除节点及其自己点的时候,大于删除节点的所有左右值都需要减去删除节点的左右差值+1;如下图示例: 删除技术部 过程: 第一步 ,计算出删除节点的左右差值+1;技术部的左右值分别时6和11,差值+1:11 – 6 + 1 第二步 ,删除节点机器所有子节点 第三步 ,所有大于删除节点左右值的节点,全部减去差值 同样为了方便操作,我们也创建一个存储过程: DROP PROCEDURE IF EXISTS removeDepartment;
  • CREATE PROCEDURE removeDepartment( IN did INT )
  • BEGIN
  • DECLARE d_error INTEGER DEFAULT 0 ;
  • DECLARE num INTEGER DEFAULT 0 ; — 删除节点左右值之间的差值
  • DECLARE dlt INT ; — 删除节点的左值
  • DECLARE drt INT ; — 删除节点的右值值
  • DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET d_error= -2 ; — 异常时设置为1
  • DECLARE CONTINUE HANDLER FOR NOT FOUND SET d_error = -3 ; — 如果表中没有下一条数据则置为2
  • START TRANSACTION ;
  • — 查询删除节点的左右值
  • SELECT lt,rt INTO dlt, drt FROM department_info2 WHERE id = did;
  • — 计算当前节点及其子节点的差值
  • SET num = drt – dlt + 1 ;
  • — 删除当前节点及其子节点
  • DELETE FROM department_info2 WHERE lt>=dlt AND rt<=drt;
  • SET d_error= ROW_COUNT ();
  • — 左右值减少对应的插值
  • UPDATE department_info2 SET lt=lt- num WHERE lt > dlt;
  • UPDATE department_info2 SET rt=rt- num WHERE rt > drt;
  • IF d_error != num/2 THEN — 判断删除的数量与当前节点+子孙节点的数量是否一致;不一致的话,直接回滚
  • ROLLBACK ;
  • ELSE
  • COMMIT ;
  • END IF ;
  • select d_error;
  • END
  • 测试: — 设计部的id = 7
  • SET @ id = 7 ;
  • call removeDepartment(@ id );

是否存在子节点(叶子节点)

无需额外的查询,直接通过节点的左右数,即可判断是否是叶子节点了;当 右数 – 左数 = 1 时,说明当前节点就属于叶子节点,否则就不是;

查询所有的下级部门

等价于:查询左数比当前节点大,右数比当前节点小,且层比当前节点大1的所有节点;

例:查询产品部(lt = 3,rt = 12,lv = 3)的所有下级部门:

 SET @lt =;  -- 节点左数
SET @rt =; -- 节点右数
SET @lv =;  -- 当先的层级
SELECT * from department_info where lt > @lt AND rt < @rt AND `lv` = @lv + 1 

查询所有的下下级部门(孙节点)

实际业务中很少会出现此需求,这里仅仅用于可行性演示;

孙节点的查询和子节点类似,仅仅层级由+1变为了+2

例:查询产品部(lt = 3,rt = 12,lv = 3)的所有下下级部门:

 SET @lt =;  -- 节点左数
SET @rt =; -- 节点右数
SET @lv =;  -- 当先的层级
SELECT * from department_info where lt > @lt AND rt < @rt AND `lv` = @lv + 2; 

查询所有下属部门

等价于:比当前节点左数大,右数小的节点,全部是子孙节点;

例,查询产品部(lt = 3,rt = 12)的所有下属部门:

 SET @lt =;  -- 节点左数
SET @rt =; -- 节点右数
SELECT * from department_info where lt > @lt AND rt < @rt; 

查询隶属部门

等价于:左数比自己小,右数比自己大,且层级-1的节点,也就是父节点

例,查询技术部(lt = 6 , rt = 11)的隶属部门:

 SET @lt =;  -- 节点左数
SET @rt =; -- 节点右数
SET @lv =;  -- 当先的层级
SELECT * from department_info where lt < @lt AND rt > @rt AND `lv` = @lv - 1 ; 

查询所有的上级部门

与查询父节点类似,只是不再需要校验层级了,所有左数比自己小,右数比自己大都是自己的祖先节点;

例,查询产品部(lt = 3,rt = 12)的所有上级部门

 SET @lt =;  -- 节点左数
SET @rt =; -- 节点右数
SELECT * from department_info where lt < @lt AND rt > @rt; 

统计所有下属部门的数量

统计子孙节点数量,无需额外查询,只需根据自己的左右数,即可计算出子节点的数量;

计算公式: (右数 – 左数 – 1 )/ 2

例,计算产品部(id = 4)的下属部门的数量:

 SELECT dep_name,(rt - lt -) / 2 as num FROM department_info2 where id = 4 

树形结构!别再用递归实现了这才是最佳的方案;更快更强更好用

3格式化数据

不管是那种方案,数据层面都是扁平的,只是通过字段逻辑表达了结构化的关系,那查询出来之后,要如何将数据结构化成树形结构之后展示呢,下面介绍递归和非递归的两种方式实现方式:

树形结构!别再用递归实现了这才是最佳的方案;更快更强更好用

  • 基础的对象 @Data
  • @RequiredArgsConstructor
  • public class LrItem {
  • @NonNull
  • private Integer id;
  • @NonNull
  • private String depName;
  • @NonNull
  • private Integer left;
  • @NonNull
  • private Integer right;
  • private Integer level;
  • private Integer parentId;
  • /**
  • * 是否是叶子
  • */
  • private Boolean isLeaf;
  • private List<LrItem> childItem;
  • }
  • 测试数据这里只是演示格式化数据,就不整那么复杂了;实际业务场景,这批数据一般都是从数据库中查询出来的。 List<LrItem> deps = new ArrayList<>();
  • deps.add( new LrItem( 1 , “董事会” , 1 , 22 ));
  • deps.add( new LrItem( 2 , “总经理” , 2 , 19 ));
  • deps.add( new LrItem( 3 , “董事会秘书” , 20 , 21 ));
  • deps.add( new LrItem( 4 , “产品部” , 3 , 12 ));
  • deps.add( new LrItem( 5 , “行政总监” , 13 , 18 ));
  • deps.add( new LrItem( 6 , “设计部” , 4 , 5 ));
  • deps.add( new LrItem( 7 , “技术部” , 6 , 11 ));
  • deps.add( new LrItem( 8 , “财务部” , 14 , 15 ));
  • deps.add( new LrItem( 9 , “行政部” , 16 , 17 ));
  • deps.add( new LrItem( 10 , “客户端” , 7 , 8 ));
  • deps.add( new LrItem( 11 , “服务端” , 9 , 10 ));
  • 整理数据 public static void init (List<LrItem> deps) {
  • // 如果数据库排序过了之后 这里就不用排序了
  • deps.sort(Comparator.comparing(LrItem::getLeft));
  • // 为计算层级 缓存节点右侧的值
  • List<Integer> rights = new ArrayList<>();
  • Map<Integer, Integer> mp = new HashMap<>();
  • // 初始化节点的层级,叶子节点 以及 父节点ID 对应的数据
  • deps.forEach(item -> {
  • if (rights.size() > 0 ) {
  • // 一旦发现本次节点右侧的值比前一次的大,说明出现层级上移了 需要移除前一个底层及的值
  • // 这里大部分情况下只存在移除前面一个值情况
  • while (rights.get(rights.size() – 1 ) < item.getRight()) {
  • rights.remove(rights.size() – 1 ); //从rights末尾去除
  • }
  • }
  • Integer _level = rights.size() + 1 ;
  • item.setLevel(_level);
  • mp.put(_level, item.getId());
  • item.setParentId(mp.containsKey(_level – 1 ) ? mp.get(_level – 1 ) : 0 ); //计算出上级部门编号
  • item.setIsLeaf(item.getLeft() == item.getRight() – 1 ); //判断是否叶子部门
  • rights.add(item.getRight());
  • });
  • System.out.println(rights);
  • System.out.println(mp);
  • }

递归整理

递归的思路比较简单清晰,就是拿到当前节点之后,在所有是节点中找自己的子节点,当所有节点都找过一遍之后,整个树形结构话的过程就完了;

我们可以结合Java 8 Stream新特性,让整个递归代码相对简单清晰;

 /**
 * @param deps 所有数据
 */public static void recursive(List<LrItem> deps) {
    init(deps);
    //获取父节点
    List<LrItem> collect = deps.stream()
            .filter(m -> m.getParentId() ==)
            .map(m ->
                    {
                        m.setChildItem(getChildrens(m, deps));
                        return m;
                    }
            ).collect(Collectors.toList());

    // 普遍请求下,根节点只会有一个,所以这里取出第一个元素,如果由多个,可根据需求调整,这里仅做测试使用
    System.out.println(JSON.toJSON(collect.get()));
}

/**
 * 递归查询子节点
 *
 * @param root 根节点
 * @param all  所有节点
 * @return 根节点信息
 */private static List<LrItem> getChildrens(LrItem root, List<LrItem> all) {
    List<LrItem> children = all.stream()
            .filter(m -> Objects.equals(m.getParentId(), root.getId()))
            .map(m -> {
                        m.setChildItem(getChildrens(m, all));
                        return m;
                    }
            ).collect(Collectors.toList());
    return children;
} 

非递归倒序整理

这是一个 以空间换时间 的方案;

此方式的特点:按层级排序后的数据,只需要一次for循环,就能整理出结构化的数据。

  • 第一步,计算出层级以及父节点ID
  • 第二步,按层级进行排序
  • 第三步,倒序从最深的节点让root节点遍历遍历过程以 Map<Integer, List<LrItem>> 的方式缓存ID及当前节点的数据,当上升一个层级之后,就将Map中缓存的关于我的自阶段取出来,保存到自己对象的 childItem 字段中
 public static void reverseFormat(List<LrItem> deps) {
    init(deps);

    deps.sort(Comparator.comparing(LrItem::getLevel));
    deps.forEach(item -> System.out.println(JSON.toJSONString(item)));

    // 临时缓存各自节点的容器
    Map<Integer, List<LrItem>> childCache = new HashMap<>();

    // 当前节点
    LrItem lrItem = null;
    //int level =;
    // 采用倒序遍历,整理各个子节点的集合
    for (int i = deps.size() -; i >= 0; i--) {
        lrItem = deps.get(i);
        Integer parentId = lrItem.getParentId();
        if (null == lrItem || null == parentId) {
            continue;
        }

        List<LrItem> childItems = childCache.get(parentId);
        if (null == childItems) {
            childCache.put(parentId, childItems = new ArrayList<>());
        }
        childItems.add(lrItem);

        // 如果不是叶子节点的时候,说明他肯定有子节点,去缓存中找到,回填回去
        if (!lrItem.getIsLeaf()) {
            childItems = childCache.get(lrItem.getId());
            childItems.sort(Comparator.comparing(LrItem::getId));
            lrItem.setChildItem(childItems);
            childCache.remove(lrItem.getId());
        }
    }

    System.out.println(JSON.toJSONString(lrItem));
} 

格式化后的数据

不管那种方式,最终都会得到以下的结构化数据;

 {
    "depName": "董事会",
    "id":,
    "isLeaf": false,
    "left":,
    "level":,
    "prientId":,
    "right":,
    "childItem": [
        {
            "depName": "总经理",
            "id":,
            "isLeaf": false,
            "left":,
            "level":,
            "prientId":,
            "right":,
            "childItem": [
                {
                    "depName": "行政总监",
                    "id":,
                    "isLeaf": false,
                    "left":,
                    "level":,
                    "prientId":,
                    "right":,
                    "childItem": [
                        {
                            "depName": "设计部",
                            "id":,
                            "isLeaf": true,
                            "left":,
                            "level":,
                            "prientId":,
                            "right":
                        },
                        {
                            "depName": "技术部",
                            "id":,
                            "isLeaf": false,
                            "left":,
                            "level":,
                            "prientId":,
                            "right":,
                            "childItem": [
                                {
                                    "depName": "客户端",
                                    "id":,
                                    "isLeaf": true,
                                    "left":,
                                    "level":,
                                    "prientId":,
                                    "right":
                                },
                                {
                                    "depName": "服务端",
                                    "id":,
                                    "isLeaf": true,
                                    "left":,
                                    "level":,
                                    "prientId":,
                                    "right":
                                }
                            ]
                        }
                    ],
                    "depName": "产品部",
                    "id":,
                    "isLeaf": false,
                    "left":,
                    "level":,
                    "prientId":,
                    "right":
                },
                {
                    "childItem": [
                        {
                            "depName": "财务部",
                            "id":,
                            "isLeaf": true,
                            "left":,
                            "level":,
                            "prientId":,
                            "right":
                        },
                        {
                            "depName": "行政部",
                            "id":,
                            "isLeaf": true,
                            "left":,
                            "level":,
                            "prientId":,
                            "right":
                        }
                    ]
                }
            ]
        },
        {
            "depName": "董事会秘书",
            "id":,
            "isLeaf": true,
            "left":,
            "level":,
            "prientId":,
            "right":
        }
    ]
} 

4比较

针对上面的详解,下来以表格的形式来更直观的对两者做一下对比:

功能父子关系方案先序数方案 新增 简单 复杂 修改 简单 复杂 删除复杂复杂 判断叶子节点复杂(除非增加编辑难度,提前整理好) 简单 查询子节点 简单简单 查询孙节点复杂 简单 查询父节点 简单简单 查询祖先节点复杂 简单 统计子孙节点数量复杂 简单 适用场景结构简单,层级少,统计少,频繁改动结构复杂,改动少,层级深,需要复杂统计

5总结

经过对两种方案常用场景的分析,发现其实各自都有自己的优缺点,所以原谅我标题稍微有点点标题党;相比起来,个人还是比较喜欢改进的先序数方案

父子关系方案适合结构相对简单、层级少的需求;

而先序数方案就更适合结构复杂,改动少,层级深,需要频繁汇总统计的需求了;

所以说,还是那句话, 没有绝对好的方案,只有合不合适的场景 ;需要更具自己业务的实际情况,酌情挑选对项目最有利的方案。

好了,今天的分享就到这里;如果有任何问题,欢迎随时交流指正!