树形结构数据存储方案对比:父子关系 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. 总结
- 父子关系方案适合结构简单、层级少的需求。
- 先序数方案适合结构复杂、改动少、层级深、需要频繁汇总统计的需求。
没有绝对好的方案,只有合不合适的场景。选择方案时,需要根据具体业务需求来决定。
热门推荐
ps如何去除图片上的水印?这5个去水印方法很简单!
新手萨满卡组攻略(掌握关键技巧)
员工"自愿加班"是否要支付加班费?企业该如何界定"加班"
Shader实战:Unity中实现贴图像素化风格
MBTI INTP逻辑学家:最聪明的人格类型?优缺点、职业选择与伴侣匹配全解析
盘点新中国五大归国科学家,及其卓越贡献与世纪风采
基于Nexus搭建npm私服的完整指南
证券投资策略:价值投资与成长投资
深中通道早高峰全网首测:从中山去科技园上班,比光明、龙岗还快?
博士留学申请失败原因有哪些?如何分析并改进?
从尺寸到品牌:电视机选购全攻略与实用窍门解析
古越龙山绍兴黄酒,来自浙江绍兴的特产,你喝过吗?
黄金新高势不可挡!降息预期与地缘危机下,金价还能飙多久?
生态环境污染的危害与应对策略
科普 | "卫龙辣条"被曝抽检不合格?辣条还能吃...
双线路DNS:原理、优势与应用场景详解
DNS网络安全防护指南:从DNSSEC到流量监控的全方位防护策略
为何主力/资金在流入,价格却在下跌?一文唠明白资金流计算的内涵
股票流入流出的含义和影响是什么?如何分析股票流入流出的数据?
姓名与车牌号的神秘联系:文化、心理与数理分析
万方数据库如何期刊检索
未来10年,六大难题或将困扰每个家庭
C++ STL算法的重要性:提高代码可读性和正确性的强大工具
《第二十条》:法情交织的深度审视
申请商标的资格和条件有何规定
庄子与儒墨:面对现实,反应各异
宠物猫梳毛指南——猫咪多大才适合梳毛?
德瑞小学校长李克宽:自主之权,内驱之力——激发孩子学习新动力
揭秘民航飞机为何需要提前半小时开始降落
明朝内阁制度:中央政治的稳定器与权力中枢