实现一个通用的树形结构构建工具
文章目录
- 1. 前言
- 2. 树结构
- 3. 具体实现逻辑
- 3.1 TreeNode
- 3.2 TreeUtils
- 3.3 例子
- 4. 小结
1. 前言
树结构的生成在项目中应该都比较常见,比如部门结构树的生成,目录结构树的生成,但是大家有没有想过,如果在一个项目中有多个树结构,那么每一个都要定义一个生成方法显然是比较麻烦的,所以我们就想写一个通用的生成树方法,下面就来看下如何来写。
2. 树结构

看上面的图,每一个节点都会有三个属性
- parentId 表示父节点 ID,根节点的父结点 ID = null
- id 表示当前节点 ID,这个 ID 用来标识一个节点
- children 是当前节点的子节点
那么上面来介绍完基本的几个属性,下面就来看下具体的实现了。
3. 具体实现逻辑
3.1 TreeNode
TreeNode 是公共节点,就是顶层父类,里面的属性就是上面图中的三个。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class TreeNode<T, V> {private T parentId;private T id;private List<TreeNode<T, V>> children;public TreeNode(T parentId, T id) {this.parentId = parentId;this.id = id;}public void addChild(TreeNode<T, V> treeNode){if(children == null){children = new ArrayList<>();}children.add(treeNode);}}
TreeNode 里面的 id 都是用的范型,其中 T 就是 id 的类型,因为这个 id 有可能是 Long、Int、String … 类型,不一定是 Long。另一个 V 就是具体的节点类型。
使用范型的好处就是扩展性高,不需要把属性写死。
3.2 TreeUtils
这个是工具类,专门实现树的构建以及一些其他的方法,下面一个一个来看。首先是创建树的方法:
/*** 构建一棵树** @param flatList* @param <T>* @param <V>* @return*/
public static <T, V extends TreeNode<T, V>> List<V> buildTree(List<V> flatList) {if (flatList == null || flatList.isEmpty()) {return null;}Map<T, TreeNode<T, V>> nodeMap = new HashMap<>();for (TreeNode<T, V> node : flatList) {nodeMap.put(node.getId(), node);}// 查找根节点List<V> rootList = new ArrayList<>();for (V node : flatList) {// 如果父节点为空,就是一个根节点if (node.getParentId() == null) {rootList.add(node);} else {// 父节点不为空,就是子节点TreeNode<T, V> parent = nodeMap.get(node.getParentId());if (parent != null) {parent.addChild(node);} else {rootList.add(node);}}}return rootList;
}
整体时间复杂度:O(n),创建的时候传入节点集合,然后返回根节点集合。里面的逻辑是首先放到一个 nodeMap 中,然后遍历传入的集合,根据 parentId 进行不同的处理。逻辑不难,看注释即可。但是创建树的时候,有时候我们希望根据某个顺序对树进行排序,比如同一层的我想根据名字或者 id 进行排序,顺序或者倒序都可以,那么就可以使用下面的方法。
/**
* 构建一棵排序树
*
* @param flatList
* @param comparator
* @param <T>
* @param <V>
* @return
*/
public static <T, V extends TreeNode<T, V>> List<V> buildTreeWithCompare(List<V> flatList, Comparator<V> comparator) {if (flatList == null || flatList.isEmpty()) {return Collections.emptyList(); // 返回空列表而不是null,这通常是一个更好的实践}// 子节点分组Map<T, List<V>> childGroup = flatList.stream().filter(v -> v.getParentId() != null).collect(Collectors.groupingBy(V::getParentId));// 找出父节点List<V> roots = flatList.stream().filter(v -> v.getParentId() == null).sorted(comparator) // 根据提供的比较器对根节点进行排序.collect(Collectors.toList());// 构建树for (V root : roots) {buildTreeRecursive(root, childGroup, comparator);}return roots;
}private static <T, V extends TreeNode<T, V>> void buildTreeRecursive(V parent, Map<T, List<V>> childGroup, Comparator<V> comparator) {List<V> children = childGroup.get(parent.getId());if (children != null) {// 对子节点进行排序children.sort(comparator);// 将排序后的子节点添加到父节点中children.forEach(parent::addChild);// 递归对子节点继续处理children.forEach(child -> buildTreeRecursive(child, childGroup, comparator));}
}
这里面是使用的递归,其实也可以使用层次遍历的方式来写,或者直接用第一个 buildTree 方法来往里面套也行。
上面这两个是关键的方法,那么下面再给出一些其他的非必要方法,比如查询节点数。下面这个方法就是获取以 root 为根的数的节点数。
/*** 查询以 root 为根的树的节点数** @param root* @param <T>* @param <V>* @return*/
private static <T, V extends TreeNode<T, V>> long findTreeNodeCount(TreeNode<T, V> root) {if (root == null) {return 0;}long res = 1;List<TreeNode<T, V>> children = root.getChildren();if (children == null || children.isEmpty()) {return res;}for (TreeNode<T, V> child : children) {res += findTreeNodeCount(child);}return res;
}
上面是传入一个根节点,获取这棵树的节点数,而下面的就是传入一个集合来分别获取节点数,里面也是调用了上面的 findTreeNodeCount 方法去获取。
/*** 查询给定集合的节点数** @param nodes 根节点集合* @param <T>* @param <V>* @return*/
public static <T, V extends TreeNode<T, V>> HashMap<V, Long> findTreeNodeCount(List<V> nodes) {if (nodes == null || nodes.isEmpty()) {return new HashMap<>(); // 返回空列表而不是null,这通常是一个更好的实践}HashMap<V, Long> map = new HashMap<>();for (V root : nodes) {map.put(root, findTreeNodeCount(root));}return map;
}
下面再给一下获取数的深度的方法。
// 查找树的最大深度
private static <T, V extends TreeNode<T, V>> int getMaxDepthV(TreeNode<T, V> root) {if (root == null || root.getChildren() == null || root.getChildren().isEmpty()) {return 1;}return 1 + root.getChildren().stream().mapToInt(TreeUtils::getMaxDepthV).max().getAsInt();
}public static <T, V extends TreeNode<T, V>> int getMaxDepth(V root) {return getMaxDepthV(root);
}
最后,我们拿到一棵树之后,肯定有时候会希望在里面查找一些具有特定属性的节点,比如某个节点名字是不是以 xx 开头 … ,这时候就可以用下面的方法。
// 查找所有具有特定属性的节点
public static <T, V extends TreeNode<T, V>> List<V> findAllNodesByProperty(TreeNode<T, V> root, Function<V, Boolean> predicate) {if (root == null) {return Collections.emptyList();}List<V> result = new ArrayList<>();// 符合属性值if (predicate.apply((V) root)) {result.add((V) root);}if (root.getChildren() == null || root.getChildren().isEmpty()) {return result;}for (TreeNode<T, V> child : root.getChildren()) {result.addAll(findAllNodesByProperty(child, predicate));}return result;
}
好了,方法就这么多了,其他方法如果你感兴趣也可以继续补充下去,那么这些方法是怎么用的呢?范型的好处要怎么体现呢?下面就来看个例子。
3.3 例子
首先我们有一个部门类,里面包括部门的名字,然后我需要对这个部门集合来构建一棵部门树。
@Data
@ToString
@NoArgsConstructor
public class Department extends TreeNode<String, Department> {private String name;public Department(String id, String parentId, String name){super(parentId, id);this.name = name;}}
构建的方法如下:
public class Main {public static void main(String[] args) {List<Department> flatList = new ArrayList<>();flatList.add(new Department("1", null, "Sales"));flatList.add( new Department("2", "1", "East Sales"));flatList.add( new Department("3", "1","West Sales"));flatList.add( new Department("4", "2","East Sales Team 1"));flatList.add( new Department("5", "2","East Sales Team 2"));flatList.add( new Department("6", "3","West Sales Team 1"));List<Department> departments = TreeUtils.buildTreeWithCompare(flatList, (o1, o2) -> {return o2.getName().compareTo(o1.getName());});Department root = departments.get(0);List<Department> nodes = TreeUtils.findAllNodesByProperty(root, department -> department.getName().startsWith("East"));System.out.println(nodes);System.out.println(TreeUtils.getMaxDepth(root));System.out.println(TreeUtils.findTreeNodeCount(nodes));}}
可以看下 buildTreeWithCompare 的输出:

其他的输出如下:
[Department(name=East Sales), Department(name=East Sales Team 2), Department(name=East Sales Team 1)]
3
{Department(name=East Sales)=3, Department(name=East Sales Team 2)=1, Department(name=East Sales Team 1)=1}
4. 小结
工具类就写好了,从例子就可以看出范型的好处了,用了范型之后只要实现类继承了 TreeNode,就可以直接用 TreeUtils 里面的方法,并且返回的还是具体的实现类,而不是 TreeNode。
如有错误,欢迎指出!!!
相关文章:
实现一个通用的树形结构构建工具
文章目录 1. 前言2. 树结构3. 具体实现逻辑3.1 TreeNode3.2 TreeUtils3.3 例子 4. 小结 1. 前言 树结构的生成在项目中应该都比较常见,比如部门结构树的生成,目录结构树的生成,但是大家有没有想过,如果在一个项目中有多个树结构&…...
数势科技:解锁数据分析 Agent 的智能密码(14/30)
一、数势科技引领数据分析变革 在当今数字化浪潮中,数据已然成为企业的核心资产,而数据分析则是挖掘这一资产价值的关键钥匙。数势科技,作为数据智能领域的领军者,以其前沿的技术与创新的产品,为企业开启了高效数据分析…...
机器学习之过采样和下采样调整不均衡样本的逻辑回归模型
过采样和下采样调整不均衡样本的逻辑回归模型 目录 过采样和下采样调整不均衡样本的逻辑回归模型1 过采样1.1 样本不均衡1.2 概念1.3 图片理解1.4 SMOTE算法1.5 算法导入1.6 函数及格式1.7 样本类别可视化理解 2 下采样2.1 概念2.2 图片理解2.3 数据处理理解2.4 样本类别可视化…...
解决 ssh connect to host github.com port 22 Connection timed out
一、问题描述 本地 pull/push 推送代码到 github 项目报 22 端口连接超时,测试连接也是 22 端口连接超时 ssh 密钥没问题、也开了 Watt Toolkit 网络是通的,因此可以强制将端口切换为 443 二、解决方案 1、测试连接 ssh -T gitgithub.com意味着无法通…...
mybatis/mybatis-plus中mysql报错
文章目录 一、sql执行正常,mybatis报错二、sql执行正常,mybatis-plus报错直接改变字段利用mybatis-plus特性处理 总结 一、sql执行正常,mybatis报错 Caused by: net.sf.jsqlparser.parser.ParseException: Encountered unexpected token: "ur" <K_ISOLATION>a…...
在ros2 jazzy和gazebo harmonic下的建图导航(cartographer和navigation)实现(基本)
我的github分支!!! 你可以在这里找到相对应的源码。 DWDROME的MOGI分支 来源于!! MOGI-ROS/Week-3-4-Gazebo-basics 学习分支整理日志 分支概述 这是一个用于个人学习的新分支,目的是扩展基本模型并添加…...
《Rust权威指南》学习笔记(五)
高级特性 1.在Rust中,unsafe是一种允许绕过Rust的安全性保证的机制,用于执行一些Rust默认情况下不允许的操作。unsafe存在的原因是:unsafe 允许执行某些可能被 Rust 的安全性检查阻止的操作,从而可以进行性能优化,如手…...
GitHub的简单操作
引言 今天开始就要开始做项目了,上午是要把git搭好。搭的过程中遇到好多好多的问题。下面就说一下git的简单操作流程。我们是使用的GitHub,下面也就以这个为例了 一、GitHub账号的登录注册 https://github.com/ 通过这个网址可以来到GitHub首页 点击中间绿色的S…...
「Mac畅玩鸿蒙与硬件54」UI互动应用篇31 - 滑动解锁屏幕功能
本篇教程将实现滑动解锁屏幕功能,通过 Slider 组件实现滑动操作,学习事件监听、状态更新和交互逻辑的实现方法。 关键词 滑动解锁UI交互状态管理动态更新事件监听 一、功能说明 滑动解锁屏幕功能包含以下功能: 滑动解锁区域:用…...
SMMU软件指南之系统架构考虑
安全之安全(security)博客目录导读 目录 5.1 I/O 一致性 5.2 客户端设备 5.2.1 地址大小 5.2.2 缓存 5.3 PCIe 注意事项 5.3.1 点对点通信 5.3.2 No_snoop 5.3.3 ATS 5.4 StreamID 分配 5.5 MSI 本博客介绍与 SMMU 相关的一些系统架构注意事项。 5.1 I/O 一致性 如…...
使用高云小蜜蜂GW1N-2实现MIPI到LVDS(DVP)转换案例分享
作者:Hello,Panda 大家晚上好,熊猫君又来了。 今天要分享的是一个简单的MIPI到LVDS(DVP)接口转换的案例。目的就是要把低成本FPGA的应用潜力充分利用起来。 一、应用背景 这个案例的应用背景是:现在还在…...
「C++笔记」unordered_map:哈希化的无序映射函数(键值对)
unordered_map 是 C 中一个经过哈希函数(Hash)处理的映射(map)容器。 本文中的map和set是差不多的,unordered_map与unordered_set也是对应的。所以不再单独写一篇了。 这里的内容建议看完本文之后再回过头来看 二者虽然…...
Linux 安装jdk
1、官网下载jdk https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html2、以tar包为例,在window或者Linux解压都可以,这里直接在win解压了,上传到服务器 3、在/usr/local/ 创建jdk目录,将jdk上传到…...
asp.net core 发布到iis后,一直500.19,IIS设置没问题,安装了sdk,文件夹权限都有,还是报错
原因就是没有安装ASP.NET Core 9.0 Runtime (v9.0.0) - Windows Hosting Bundle,我是只安装了.net core的sdk,下面介绍下sdk和hosting bundle的关系 在 .NET Core 和 ASP.NET Core 的开发中,SDK(Software Development Kit&#x…...
【Go】运行自己的第一个Go程序
运行自己的第一个Go程序 一、Go语言的安装Go环境安装查看是否安装成功配置GOPROXY(代理) 二、Goland安装三、Goland破解四、新建项目 开一篇专栏记录学习Go的过程,一门新语言从hello world开始,这篇文章详细讲解Go语言环境搭建及hello world实现 一、Go语…...
qt qss文件的使用
qt样式的修改方式 一 通过ui界面的改变样式表来直接修改显示效果。 不推荐,其他人不好修改,不够直观,不易维护。 二 通过setStyleSheet接口修改。 一般,界面很少的时候可以使用。一旦界面多起来,代码部分就显得杂乱…...
【管道——二分+区间合并】
题目 思路 区间合并 1、按照左端点排序2、遍历窗口,若窗口非法,继续遍历;否则执行33、若是第一个窗口,设定合并结果初值,判断结果左端点是否造成“起点过大”,是,FALSE退出;否则执行…...
宽带、光猫、路由器、WiFi、光纤之间的关系
1、宽带(Broadband) 1.1 宽带的定义宽带指的是一种高速互联网接入技术,通常包括ADSL、光纤、4G/5G等不同类型的接入方式。宽带的关键特点是能够提供较高的数据传输速率,使得用户可以享受到稳定的上网体验。 1.2 宽带的作用宽带是…...
如何排查 Apache Doris 中 “Failed to commit txn“ 导入失败问题?
今天来聊聊 Doris 数据导入那些事儿。你是不是在数据导入的时候遇到各种状况,让人头疼不已?别担心,这篇文章给你答案! 在 Doris 的版本里,< 2.0.3 的时候,数据迁移存在一些已知的问题,比如可…...
回归预测 | MATLAB实现CNN-GRU卷积门控循环单元多输入单输出回归预测
回归预测 | MATLAB实现CNN-GRU卷积门控循环单元多输入单输出回归预测 目录 回归预测 | MATLAB实现CNN-GRU卷积门控循环单元多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 MATLAB实现CNN-GRU卷积门控循环单元多输入单输出回归预测 数据准备&#x…...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
微信小程序云开发平台MySQL的连接方式
注:微信小程序云开发平台指的是腾讯云开发 先给结论:微信小程序云开发平台的MySQL,无法通过获取数据库连接信息的方式进行连接,连接只能通过云开发的SDK连接,具体要参考官方文档: 为什么? 因为…...
[Java恶补day16] 238.除自身以外数组的乘积
给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法,且在 O(n) 时间复杂度…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
Go语言多线程问题
打印零与奇偶数(leetcode 1116) 方法1:使用互斥锁和条件变量 package mainimport ("fmt""sync" )type ZeroEvenOdd struct {n intzeroMutex sync.MutexevenMutex sync.MutexoddMutex sync.Mutexcurrent int…...
数据结构第5章:树和二叉树完全指南(自整理详细图文笔记)
名人说:莫道桑榆晚,为霞尚满天。——刘禹锡(刘梦得,诗豪) 原创笔记:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 上一篇:《数据结构第4章 数组和广义表》…...
云原生安全实战:API网关Envoy的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关 作为微服务架构的统一入口,负责路由转发、安全控制、流量管理等核心功能。 2. Envoy 由Lyft开源的高性能云原生…...
