树形结构数据存储方案对比:父子关系 vs 改进后的先序树
创作时间:
作者:
@小白创作中心
树形结构数据存储方案对比:父子关系 vs 改进后的先序树
引用
1
来源
1.
https://cloud.tencent.com/developer/article/2093198
在处理树形结构数据时,通常会遇到节点的增删改、查询子节点、统计数量等需求。本文将通过一个简单的公司组织架构示例,详细讲解两种常见的树形结构设计方案:父子关系方案和改进后的先序树方案,并对比它们的实现方式和优缺点。
1. 父子关系方案
父子关系方案的核心思想是每个节点只关注自己的父节点,并通过递归方式获取层级关系。这种方案简单易懂,但当数据量大、层级深时,性能会受到影响。
方案特点
优点
方案简单易懂
数据结构简单清晰
层级直观、鲜明
易维护(层级关系只需要关注自己的父ID)
缺点
查找麻烦、统计麻烦(需要递归逐层查找)
示例
以一个简单的公司组织架构为例:
对应的表结构如下:
ID | dep_name(部门名称) | level(层级) | parent_id(父ID) |
---|---|---|---|
1 | 董事会 | 1 | 0 |
2 | 总经理 | 2 | 1 |
3 | 董事会秘书 | 2 | 1 |
4 | 产品部 | 3 | 2 |
5 | 行政总监 | 3 | 2 |
6 | 设计部 | 4 | 4 |
7 | 技术部 | 4 | 4 |
8 | 财务部 | 4 | 5 |
9 | 行政部 | 4 | 5 |
10 | 客户端 | 5 | 7 |
11 | 服务端 | 5 | 7 |
SQL脚本:
DROP TABLE IF EXISTS `department_info`;
CREATE TABLE `department_info` (
`id` int(11) NOT NULL,
`dep_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '名称',
`level` int(11) NULL DEFAULT NULL,
`parent_id` int(11) NULL DEFAULT NULL COMMENT '父ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `department_info` VALUES (1, '董事会', 1, 0);
INSERT INTO `department_info` VALUES (2, '总经理', 2, 1);
INSERT INTO `department_info` VALUES (3, '董事会秘书', 2, 1);
INSERT INTO `department_info` VALUES (4, '产品部', 3, 2);
INSERT INTO `department_info` VALUES (5, '行政总监', 3, 2);
INSERT INTO `department_info` VALUES (6, '设计部', 4, 4);
INSERT INTO `department_info` VALUES (7, '技术部', 4, 4);
INSERT INTO `department_info` VALUES (8, '财务部', 4, 5);
INSERT INTO `department_info` VALUES (9, '行政部', 4, 5);
INSERT INTO `department_info` VALUES (10, '客户端', 5, 7);
INSERT INTO `department_info` VALUES (11, '服务端', 5, 7);
为了方便查询,可以创建两个递归函数:
- 递归查询子孙节点ID的函数
- 递归查询祖先节点ID的函数
2. 改进后的先序树方案
改进后的先序树方案通过为每个节点增加左值和右值来优化查询性能。这种方案避免了递归查询,但在节点增删改时需要维护左右值,相对复杂。
方案特点
优点
查询汇总简单高效
无需递归查询,性能高
缺点
结构相对复杂,数据层面理解难度较高
不易维护(左右值的存在会影响后续节点)
示例
对应的表数据如下:
id | dep_name(部门名称) | lt(左值) | rt(右值) | lv(层级) |
---|---|---|---|---|
1 | 董事会 | 1 | 22 | 1 |
2 | 总经理 | 2 | 19 | 2 |
3 | 董事会秘书 | 20 | 21 | 2 |
4 | 产品部 | 3 | 12 | 3 |
5 | 行政总监 | 13 | 18 | 3 |
6 | 设计部 | 4 | 5 | 4 |
7 | 技术部 | 6 | 11 | 4 |
8 | 财务部 | 14 | 15 | 4 |
9 | 行政部 | 16 | 17 | 4 |
10 | 客户端 | 7 | 8 | 5 |
11 | 服务端 | 9 | 10 | 5 |
SQL语句:
DROP TABLE IF EXISTS `department_info2`;
CREATE TABLE `department_info2` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dep_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
`lt` int(11) NOT NULL COMMENT '节点左数',
`rt` int(11) NOT NULL COMMENT '节点右数',
`lv` int(11) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `department_info2` VALUES (1, '董事会', 1, 22, 1);
INSERT INTO `department_info2` VALUES (2, '总经理', 2, 19, 2);
INSERT INTO `department_info2` VALUES (3, '董事会秘书', 20, 21, 2);
INSERT INTO `department_info2` VALUES (4, '产品部', 3, 12, 3);
INSERT INTO `department_info2` VALUES (5, '行政总监', 13, 18, 3);
INSERT INTO `department_info2` VALUES (6, '设计部', 4, 5, 4);
INSERT INTO `department_info2` VALUES (7, '技术部', 6, 11, 4);
INSERT INTO `department_info2` VALUES (8, '财务部', 14, 15, 4);
INSERT INTO `department_info2` VALUES (9, '行政部', 16, 17, 4);
INSERT INTO `department_info2` VALUES (10, '客户端', 7, 8, 5);
INSERT INTO `department_info2` VALUES (11, '服务端', 9, 10, 5);
3. 格式化数据
无论是哪种方案,数据层面都是扁平的,需要通过递归或非递归方式将其结构化成树形结构。
递归整理
使用Java 8 Stream新特性简化递归代码:
public static void recursive(List<LrItem> deps) {
init(deps);
List<LrItem> collect = deps.stream()
.filter(m -> m.getParentId() == 0)
.map(m -> {
m.setChildItem(getChildrens(m, deps));
return m;
})
.collect(Collectors.toList());
System.out.println(JSON.toJSON(collect.get(0)));
}
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;
}
非递归倒序整理
这是一个以空间换时间的方案:
public static void reverseFormat(List<LrItem> deps) {
init(deps);
deps.sort(Comparator.comparing(LrItem::getLevel));
Map<Integer, List<LrItem>> childCache = new HashMap<>();
LrItem lrItem = null;
for (int i = deps.size() - 1; i >= 0; i--) {
lrItem = deps.get(i);
Integer parentId = lrItem.getParentId();
if (parentId != null) {
List<LrItem> childItems = childCache.get(parentId);
if (childItems == null) {
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));
}
4. 比较
功能 | 父子关系方案 | 先序数方案 |
---|---|---|
新增 | 简单 | 复杂 |
修改 | 简单 | 复杂 |
删除 | 复杂 | 复杂 |
判断叶子节点 | 复杂(除非增加编辑难度,提前整理好) | 简单 |
查询子节点 | 简单 | 简单 |
查询孙节点 | 复杂 | 简单 |
查询父节点 | 简单 | 简单 |
查询祖先节点 | 复杂 | 简单 |
统计子孙节点数量 | 复杂 | 简单 |
5. 总结
- 父子关系方案适合结构简单、层级少的需求。
- 先序数方案适合结构复杂、改动少、层级深、需要频繁汇总统计的需求。
没有绝对好的方案,只有合不合适的场景。选择方案时,需要根据具体业务需求来决定。
热门推荐
30个VSCode插件,让你的开发效率大幅提升
八字十神论命七杀 八字十神七杀解析
诗歌意象的十二条表现形式,并从现代诗中汲取灵感与智慧
西安地铁线路图及运营时间指南
第一次数学危机:无理数的发现如何颠覆了古希腊数学信仰体系?
如何保障医生的合法权益
家常鱼,如何做到既好吃又简单,确实需要一些技巧和心得
盘山路开车技巧全攻略:安全行驶要点与注意事项
菜板多久换一次比较好?菜板最好选择什么材质的?
首都师范大学“乡音何处寻”小组深入方言研究,探索方言保护路径
医保门诊挂号费可以报销吗,详解医保政策和报销流程
男命八字中劫财的意义 男命劫财解析
皮脂腺囊肿的治疗方法及日常护理指南
老人血压低是什么原因怎么调理
飞机航班取消了保险不理赔怎么办?申请赔偿指南及法律依据
高等教育教师资格证报考条件及报名流程指南
美国公立常春藤大学有哪些?全面解析顶尖公立学府
如何购买工龄以保障权益?这种购买方式有哪些注意事项?
借鉴成功人士财务管理方法,提升财富管理能力
跳绳的注意事项及技巧
共促儿童心理健康,中国逾50城为庆祝世界儿童日而“点亮”
感觉狗智商低怎么办?提升狗狗智力的有效方法是什么?
机器学习数据集的训练、验证、测试拆分
胆碱能性荨麻疹的全面管理:从诱因控制到药物治疗
消费“年轻化”,首先要尊重青春期心理需求
国产丧尸生存游戏《遗骸》:深度解析重庆末日世界的魅力与问题
中小学提高记忆力书籍排名
日本虚拟亚文化对中国虚拟主播的影响及其未来发展规划
牛腰子怎么吃好吃又简单
低热量饮食是否可以降低血脂