树形结构构建的两种方式
树形结构(Tree Structure)是一种常用的数据结构,用于表示具有层次关系的数据集。树形结构由节点(Nodes)组成,这些节点通过边(Edges)相互连接。每个节点可以有零个或多个子节点,但只有一个父节点(除了根节点外)。
一,树形结构的基本概念
- 节点(Node) :
- 每个树的组成部分,可以包含数据和其他信息。
- 每个节点可以有零个或多个子节点。
- 根节点(Root Node) :
- 树的顶部节点,没有父节点。
- 整棵树的起点。
- 子节点(Child Node) :
- 每个节点可以拥有一个或多个子节点。
- 子节点是从父节点延伸出来的。
- 父节点(Parent Node) :
- 每个节点(除了根节点)都有一个直接的父节点。
- 父节点是子节点的直接上级节点。
- 叶子节点(Leaf Node) :
- 没有子节点的节点。
- 位于树的最底层。
- 路径(Path) :
- 从一个节点到另一个节点的一系列连续的边。
- 深度(Depth) :
- 一个节点到根节点的路径长度。
- 根节点的深度为 0。
- 高度(Height) :
- 树的高度是指从根节点到最远叶子节点的最长路径长度。
- 树的高度至少为 0(仅包含根节点的树)。
二,使用场景
这种树形结构在多种应用场景中都非常有用,例如:
- 行政区域划分 :
- 将省份、城市、区县等组织成树形结构,便于管理和查询。
- 组织结构 :
- 公司内部的部门和职位关系,可以表示为树形结构。
- 文件系统 :
- 计算机文件系统的目录结构就是一个典型的树形结构。
- 产品分类 :
- 电子商务网站中的商品分类通常也是树形结构。
- 权限管理 :
- 用户权限和角色管理中,权限和角色之间的关系可以用树形结构表示。
- 菜单导航 :
- 网站或应用程序中的多级菜单结构。
三,简单树形结构的创建方式
这里我使用省市县为代表来进行代码实现
1.递归
------(不太推荐)
递归的思路是从顶级节点开始,不断寻找其子节点,并将这些子节点递归地构建到对应的父节点下。
以下是递归版本的代码实现:
java
@GetMapping("/cityTree") // 将该方法映射为 HTTP GET 请求,访问路径为 /cityTree
public List<City> cityTree() {
// 从 cityService 中获取所有城市的列表
List<City> list = cityService.list();
// 创建一个 HashMap,用于快速查找城市对象
HashMap<Integer, City> baseMap = new HashMap<>();
// 将所有城市对象按 ID 存入 baseMap
for (City city : list) {
baseMap.put(city.getId(), city);
}
// 返回顶级城市列表,通过递归的方式构建子节点
return buildCityTree(0, baseMap);
}
/**
* 递归构建城市树形结构
*
* @param parentId 父节点 ID
* @param baseMap 存储所有城市对象的 Map
* @return 父节点的子城市列表
*/
private List<City> buildCityTree(int parentId, HashMap<Integer, City> baseMap) {
List<City> children = new ArrayList<>();
// 遍历 baseMap 中的所有城市对象
for (City city : baseMap.values()) {
// 如果当前城市的父 ID 与传入的 parentId 匹配,则将其视为当前节点的子节点
if (city.getParentId() == parentId) {
// 递归构建当前城市的子节点
city.setChildren(buildCityTree(city.getId(), baseMap));
// 将当前城市添加到父节点的子城市列表中
children.add(city);
}
}
return children; // 返回当前节点的子节点列表
}
递归的优缺点
优点:
- 代码简洁易读:递归的代码通常更简洁和易于理解,符合人的逻辑思维,直接表达出层级结构的构建过程。
- 模块化强:递归将问题分解成子问题,天然符合模块化的设计思想,便于维护和扩展。
- 符合层次遍历的自然方式:递归非常适合树形和层次结构的数据处理,代码中直接展现了父子关系的递进性。
缺点:
- 内存开销较大:递归调用会占用系统的栈空间,当数据量大或者层级深度过大时,可能导致栈溢出(StackOverflowError)。
- 性能问题:递归涉及多次函数调用,调用栈的创建和销毁会带来一定的性能开销,尤其是在大量数据或深度递归时,性能可能不如迭代方式。
- 调试困难:递归程序的执行顺序较复杂,调试时不容易跟踪每次调用的状态和变化,容易出错。
递归适用于层次结构明显、数据规模适中的场景。当数据规模较大时,需小心栈空间的限制,可能需要优化或使用其他方式(如显式栈的迭代法)来替代递归。
2.借助Map循环构建
java
@GetMapping("/cityTree") // 将该方法映射为 HTTP GET 请求,访问路径为 /cityTree
public List<City> cityTree() {
// 从 cityService 中获取所有城市的列表
List<City> list = cityService.list();
// 创建一个 HashMap,用于存储城市对象,以城市的 ID 作为键,城市对象作为值
HashMap<Integer, City> baseMap = new HashMap<>();
// 遍历所有城市,将城市对象存入 baseMap 中
for (City city : list) {
baseMap.put(city.getId(), city);
}
// 创建一个 List,用于存储最终的城市树形结构
List<City> cityList = new ArrayList<>();
// 再次遍历城市列表,将城市按照层级关系组装为树形结构
for (City city : list) {
// 判断是否为顶级父类(根节点),即 parentId 为 0
if (city.getParentId() == 0) {
cityList.add(city); // 将顶级城市添加到 cityList 中
continue; // 继续下一次循环
}
// 如果不是顶级城市,则将其作为子节点添加到对应的父节点下
City parent = baseMap.get(city.getParentId()); // 获取当前城市的父城市对象
List<City> children = parent.getChildren(); // 获取父城市的子城市列表
// 如果子城市列表为空,则初始化子城市列表
if (children == null) {
children = new ArrayList<>();
parent.setChildren(children); // 将初始化的子城市列表设置回父城市对象中
}
// 将当前城市添加到父城市的子城市列表中
children.add(city);
}
// 返回组装好的树形城市列表
return cityList;
}
功能说明
该方法的主要功能是将一组城市数据根据父子关系组装成树形结构,并返回顶层的城市列表。树形结构意味着每个城市对象可能包含一个子城市列表,这样的层次结构反映了城市间的层级关系(比如省市区的关系)。
实现原理
- 数据获取与初始化 :首先通过
cityService.list()
获取所有城市的列表,并将所有城市放入baseMap
中,便于快速通过 ID 查找城市对象。 - 城市关系构建 :遍历城市列表,根据每个城市的
parentId
判断其是否为顶级节点。如果是顶级节点(parentId == 0
),直接添加到cityList
中;否则,将其作为子节点添加到对应父节点的children
列表中。 - 结果返回 :最终返回包含树形结构的顶级城市列表
cityList
。
优缺点
优点:
- 高效的父子关系查找 :通过
HashMap
快速查找父节点,时间复杂度为 O(1),大大提高了构建树形结构的效率。 - 结构清晰:代码逻辑分明,容易理解,尤其适合树形结构的构建和展示。
缺点:
- 内存占用较大 :需要额外的
HashMap
来存储城市数据,增加了一定的内存开销。 - 数据完整性依赖外部数据源 :假设
cityService.list()
返回的数据是完整且无误的,未考虑数据异常或缺失的情况,比如缺少某些父节点。 - 无法处理循环依赖:如果城市数据存在循环依赖(如子节点指向了自己的父节点),可能导致逻辑错误或无限循环,需额外校验数据的合法性。
综合对比
比较维度 | 借助Map循环构建方法 | 递归构建方法 |
---|---|---|
代码简洁度 | 逻辑较多,代码稍显复杂 | 代码简洁,符合自然层次结构 |
性能 | 较高效,无递归栈调用开销 | 存在递归栈调用开销,较大数据量时性能下降 |
内存使用 | 需要额外的 HashMap ,占用较多内存 |
占用内存较少,但深度过大会消耗栈空间 |
可维护性 | 逻辑清晰,容易理解和调试 | 递归过程复杂,调试和错误处理较难 |
安全性 | 无栈溢出风险,适合深层次复杂结构 | 存在栈溢出风险,不适合深度递归 |
总结
- 选择递归:如果数据层次结构清晰且层次较少,递归的实现会更简洁直观,易于维护。
- 选择循环:当面对大数据量和复杂层次的情况时,循环构建方法更为稳定和高效,避免了递归的局限性。
两种方法各有优缺点,选择时应根据实际数据规模、层次深度和对性能的需求综合考虑。