拓扑排序在处理树形关系结构中的应用

news2025/4/7 11:37:21

Preface


偶然在QQ上的一个交流群中看到了一位群友的棘手需求。互联网开发中,数据的落盘存储通常在MySQL中。MySQL是一种关系型数据库,以“行”为基本的存储单元,然后通过外键等建立数据实体模型之间的联系。

但有些数据的存储,在MySQL上并没有那么友好。比如一个存在等级与隶属关系的部门表。每个部门会有一个所直接隶属的上级部门,一个部门又可能会有多个直属的下级部门。在MySQL中存储时,每一个部门都要有一个parent列,来指明自己的直属上级部门。这样一来,就会得到一张实际为树形关系的部门结构表。

实体数据之间的关系,是一张树形结构图

话题回到群友的需求上来,群友的需求也很简单,通过数据库查询与业务层的处理,将所有部门的完整隶属关系整合成一个列表,如下所示:

0: 北京分公司 -> 人事部 -> 档案组
1: 北京分公司 -> 人事部 -> 绩效组
2: 北京分公司 -> 科技部 -> 前台组
             ...

处理思路


看到了这样的需求后,第一个闪念在脑海中的想法是——拓扑排序。我们知道树结构是图结构的一种特例,树结构是一种无环图,因此只要数据库记录层面不出问题,将查询得到的数据按照树结构组织起来,那么该结构的拓扑序列是一定存在的。

在实际拓扑排序中,只要将每条数据的parent域作为一条指向其他结点的边来看待即可。从入度为零的结点出发,按照parent指针进行深度优先遍历,将路径上的结点收集起来,即可完成上述需求。

如果想要了解拓扑排序的过程或者算法原理,可以查看数据结构书籍中关于章节的讲述,也可查看下面链接中的解释。

拓扑排序百度百科:https://baike.baidu.com/item/%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F

拓扑排序维基百科:https://zh.wikipedia.org/wiki/%E6%8B%93%E6%92%B2%E6%8E%92%E5%BA%8F

设计与实现


算法性能预估

在拓扑排序之前,我们需要考虑一下算法的性能问题。首先拓扑排序的时间复杂度在 O ( N 2 ) O(N^2) O(N2),因此理论上该算法的性能并不好。但是时间复杂度只是一个理论指标,实际我们还需要考虑问题的规模。通常部门的架构表的数据量不会很大,对于某些场景下,该表的数据量也就在几千到几万条之间。

同时,部门架构表经常面对的是读多写少的场景,因此一定程度上,我们可以将一次业务处理的数据进行缓存。而且在业务处理中,面对如此小量的数据,通常我们可以从磁盘中一次性将其读入到内存,然后供业务层处理,这样可以减少磁盘IO的次数。

算法实现

准备承载数据库查询结果的POJO类

@Data  
@NoArgsConstructor  
@AllArgsConstructor  
static class Node {  
    private Integer id;            // 部门ID  
    private String name;           // 部门名称  
    private Integer parent;        // 上级部门的ID  
    private Integer inDegree = 0;  // 该部门的入度(该部门子部门的数量)  
}

模拟数据库查询得到整张部门表数据

final List<Node> dbQuerySet = Arrays.asList(  
        new Node(1, "北京分公司", null, 0),  
        new Node(2, "人事部", 1, 0),  
        new Node(3, "科技部", 1, 0),  
        new Node(4, "售后部", 1, 0),  
        new Node(5, "档案组", 2, 0),  
        new Node(6, "绩效组", 2, 0),  
        new Node(7, "前台组", 3, 0),  
        new Node(8, "中台组", 3, 0),  
        new Node(9, "后台组", 3, 0),  
        new Node(10, "回访组", 4, 0),  
        new Node(11, "话务组", 4, 0),  
        new Node(12, "客服中心", 4, 0),  
        new Node(13, "纪律检查小组", null, 0),  
        new Node(14, "总公司驻派审计小组", null, 0),  
        new Node(15, "作风违纪监察委员会", 13, 0),  
        new Node(16, "财务违纪监察委员会", 13, 0),  
        new Node(19, "制度研判委员会", 13, 0)  
);

将查询到的数据组织为树结构,并计算每个部门的入度

// 0. 将数据组织为树结构  
final Map<Integer, Node> map = new HashMap<>();  
// 1. 将所有的查询结果放入Map中缓存,以部门ID为查询键  
dbQuerySet.forEach(it -> map.put(it.getId(), it));  
// 2. 建立每个结点的入度  
dbQuerySet.forEach(it -> {  
    final Integer p = it.getParent();  
    if (p == null) return;  
    map.compute(p, (id, node) -> { node.setInDegree(node.getInDegree() + 1); return node; });  
});

按照拓扑排序的思想,实现上文中的要求

// 3. 开始构建  
final List<String> deptTopologicalGraph = new ArrayList<>(); // 存储最终的结果  
final Deque<String> path = new ArrayDeque<>(); // 存储每次深度优先搜索路径上的结点  
for (;;) {  
    if (map.isEmpty()) break;  
    Node t = null;  
    //  3.1 找到入度为0的结点  
    for (final Map.Entry<Integer, Node> e : map.entrySet()) {  
        if (Objects.equals(e.getValue().getInDegree(), 0)) {  
            t = map.remove(e.getKey());  
            break;  
        }    
    }    
    
    Objects.requireNonNull(t, "部门架构中存在环路");  
    if (t.getParent() == null) continue; // 排除汇点  
  
    // 3.2 按照parent域不断向上寻找  
    for (;;) {  
        path.push(t.getName());  
        final Integer pId = t.getParent(); // 获取父结点的ID  
        if (pId == null) break;  
        final Node pn = map.get(pId); // 获取父结点  
        Objects.requireNonNull(pn, "父结点不能为空");  
  
        if (Objects.equals(t.getInDegree(), 0)) {  
            // 若当前结点的入度为零,则将该结点从Map中删除  
            //  同时将当前结点的父结点入度值减1  
            pn.setInDegree(pn.getInDegree()-1);  
            map.remove(t.getId());  
        }        
        t = pn;    
    }    
    deptTopologicalGraph.add(String.join(" -> ", path)); // 生成结果  
    path.clear(); // 清空path  
}  
deptTopologicalGraph.forEach(System.out::println);

算法执行结果

北京分公司 -> 人事部 -> 档案组
北京分公司 -> 人事部 -> 绩效组
北京分公司 -> 科技部 -> 前台组
北京分公司 -> 科技部 -> 中台组
北京分公司 -> 科技部 -> 后台组
北京分公司 -> 售后部 -> 回访组
北京分公司 -> 售后部 -> 话务组
北京分公司 -> 售后部 -> 客服中心
纪律检查小组 -> 作风违纪监察委员会
纪律检查小组 -> 财务违纪监察委员会
纪律检查小组 -> 制度研判委员会

算法存在的问题与解决方案

从上述算法执行结果上来看,部门中存在多个一级部门(没有上级的部门)时,算法也可以正常工作。但是若一个一级部门没有其下属部门时,最终的结果并不会将其收纳进来。因此在算法开始之前,可以先将这些作为一级部门但没有下属部门的特例单独过滤出来处理。

// 3.0 预处理一些特例情况  
final Map<Integer, Node> newMap = map.entrySet().stream()  
        .filter(it -> {  
            final Node n = it.getValue();  
            if (n.getParent() == null && n.getInDegree() == 0) {  
                deptTopologicalGraph.add(n.getName());  
                return false;  
            } else {  
                return true;  
            }        
        })        
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

正确的输出结果

总公司驻派审计小组
北京分公司 -> 人事部 -> 档案组
北京分公司 -> 人事部 -> 绩效组
北京分公司 -> 科技部 -> 前台组
北京分公司 -> 科技部 -> 中台组
北京分公司 -> 科技部 -> 后台组
北京分公司 -> 售后部 -> 回访组
北京分公司 -> 售后部 -> 话务组
北京分公司 -> 售后部 -> 客服中心
纪律检查小组 -> 作风违纪监察委员会
纪律检查小组 -> 财务违纪监察委员会
纪律检查小组 -> 制度研判委员会

可优化的点

可以在数据库存储时,为每一行数据新增一个入度(in_degree)列,这样在查询时,可以将某些特例单独查询处理,同时也减少了业务层面关于入度的一些计算工作。

-- 查询特例
select * from dept where parent = NULL and in_degree = 0;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/532803.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

波奇学C++:友元函数,友元类,内部类,匿名对象,优化构造

友元函数/类 &#xff1a;突破访问限定符&#xff0c;允许非同一个类的函数或者类访私有成员变量。 class A { public:A():_a(0),_b(1){cout << "A()" << endl;} private:int _a 0;int _b 1; }; void fun(const A& a) {cout << a._a <&l…

canal学习-运行canal-adapter源码并记录解决报错问题(一)

运行canal-adapter 1. 下载canal源码1.1 下载源码并安装好环境1.2 查看目录结构 2.项目运行2.1 项目打包2.2 项目打包可能遇到的问题&#xff1a;1.Failure to find com.alibaba.otter:connector.tcp:jar:jar-with-dependencies:1.1.52.com.alibaba.druid.pool.DruidDataSource…

HTTP协议与TCP协议

HTTP协议 1. HTTP有哪些⽅法&#xff1f; HTTP 1.0 标准中&#xff0c;定义了3种请求⽅法&#xff1a;GET、POST、HEAD HTTP 1.1 标准中&#xff0c;新增了请求⽅法&#xff1a;PUT、PATCH、DELETE、OPTIONS、TRACE、CONNECT 2. 各个HTTP方法的具体作用是什么&#xff1f; 方…

量子计算:揭开未来计算世界的面纱

随着科技的飞速发展&#xff0c;计算能力的提升成为人们关注的焦点之一。而在这个领域中&#xff0c;量子计算正逐渐成为备受瞩目的新星。量子计算利用了量子力学的原理&#xff0c;与传统计算方式有着根本的不同。在传统计算中&#xff0c;信息以比特的形式表示&#xff0c;而…

COMSOL光电专题第三十三期(线上),COMSOL声学(北京线下)专题线上通知

背景&#xff1a; COMSOL多物理场仿真软件以高效的计算性能和杰出的多场耦合分析能力实现了精确的数值仿真&#xff0c;已被广泛应用于各个领域的科学研究以及工程计算&#xff0c;为工程界和科学界解决了复杂的多物理场建模问题。COMSOL内嵌的声学模块可以方便地进行多孔声学…

AI技术如何助力合同智能管理?

近年来&#xff0c;合同管理领域开始大规模应用AI技术&#xff0c;今天我们来关注下AI技术如何助力合同智能管理&#xff1f; 传统的合同管理系统&#xff0c;一般都是流程管理&#xff0c;随着AI技术的快速发展&#xff0c;AI技术已经成功应用到了合同全生命周期管理的各阶段…

计算机网络 三 (数据链路层)上

数据链路层 数据链路层的概述 基本概念 数据链路层是OSI参考模型中的第二层&#xff0c;它主要负责在物理层上提供可靠的数据传输服务&#xff0c;使得相邻节点间的数据传输能够实现。 数据链路层的基本概念如下&#xff1a; 帧&#xff1a;数据链路层数据传输的基本单位是…

Redis持久化:RDB和AOF(版本redis 7.0)

什么是持久化&#xff1f; 学过计算机基础的都知道以一种磁盘&#xff0c;只要关机&#xff0c;那么磁盘的内容都会被清空&#xff0c;这种磁盘称为内存&#xff0c;而Redis则是一种内存数据库&#xff0c;redis中的数据也都存储在磁盘中&#xff0c;如果服务器中进程被关掉&am…

麻了,真的不想做测试了...

前言 有不少技术友在测试群里讨论&#xff0c;近期的面试越来越难了&#xff0c;要背的八股文越来越多了,考察得越来越细&#xff0c;越来越底层&#xff0c;明摆着就是想让我们徒手造航母嘛&#xff01;实在是太为难我们这些测试工程师了。 这不&#xff0c;为了帮大家节约时…

Mathtype修改硕士论文公式格式

Mathtype修改硕士论文格式 1将word格式的公式变为mathtype格式1选中公式2点击mathtype中的转换公式 2修改mathtype格式的公式文字版式 1将word格式的公式变为mathtype格式 1选中公式 如果不选公式默认全文所有公式或者指定的公式。 2点击mathtype中的转换公式 选择要转换的…

汽车功能安全

前言 近些年来&#xff0c;功能安全在汽车传统底盘域和动力域的应用已较为成熟&#xff0c;各大汽车企业功能安全意识也逐渐增强。在辅助驾驶和自动驾驶爆发式增长的大趋势下&#xff0c;现代汽车的功能安全在目前尤为复杂的电子电气系统中就显得更为重要&#xff0c;功能安全…

MySQL---存储过程流程控制(判断(if、case)、循环(while、repeat、loop))

1. if判断 IF语句包含多个条件判断&#xff0c;根据结果为TRUE、FALSE执行语句&#xff0c;与编程语言中的if、else if、else 语法类似&#xff0c;其语法格式如下&#xff1a; -- 语法 if search_condition_1 then statement_list_1[elseif search_condition_2 then statem…

十八、Stream 流

目录 1、为什么要引入SpringCloud Stream 2、SpringCloud Stream简介 2.1、标准MQ架构图 2.2、SpringCloud Stream架构图 2.3、SpringCloud Stream处理流程 3、如何使用SpringCloud Stream 3.1、创建springcloud-stream-sender项目&#xff08;消息生产者&#xff09; …

SpringCloud_服务调用_Ribbon负载算法简介与如何替换(二)

SpringCloud_服务调用_Ribbon负载算法简介与如何替换(二) Ribbon核心组件IRule IRule:根据特定算法中从服务列表中选取一个要访问的服务 IRule接口有多种实现&#xff1a; Ribbon自带的7种负载规则 com.netflix.loadbalancer.RoundRobinRule 轮询 com.netflix.loadbalancer.Ra…

8年测试老鸟总结,软件测试工程师关键成长晋升要素,这些不能不知道...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、技术-依然是最…

MySQL深入浅出:自增长序列(@i:=@i+1)的用处及用法

目录 1&#xff1a;问题分析 2&#xff1a;模拟自增长序列 2.1 单表查询 2.2 多表关联查询 3&#xff1a;结束语 社区 1&#xff1a;问题分析 Oracle中的伪列 ROWNUM 是一组递增的序列&#xff0c;在查询数据时生成&#xff0c;为结果集中每一行标识一个行号, 每条记录…

医院运维场景下的风险感知

随着医疗信息化建设发展&#xff0c;医院的系统、设备不断叠加&#xff0c;在提升用户体验&#xff0c;享受高效医疗服务的同时&#xff0c;也为支撑系统稳定运行的信息部门带来巨大挑战。诸如科室复杂、应用场景多、终端运维工作量大、软件系统兼容需求强等痛点&#xff0c;并…

WPF MaterialDesign 初学项目实战(0):github 项目Demo运行

前言&#xff1a; 最近在学B站的WPF项目实战合集(2022终结版)&#xff0c;但是到22P时候发现UI框架 MaterialDesignThemes的Github上面的程序没办法正常运行&#xff0c;最后折腾了好久终于解决。 github地址 gitcode镜像地址 下载成功后 下载成功后是如下效果 打开这个文…

259元,诺基亚返祖式新机玩了把大的

智能手机经过这么多年发展&#xff0c;市场逐渐趋向成熟。 在这个过程中&#xff0c;优胜劣汰这一自然法则同样适用&#xff0c;无数没能经受住市场考验的企业也只能含泪离场。 其中最典型的例子还得是曾经手机中的王者诺基亚了。 当初在 Symbian&#xff08;塞班系统&#x…

php event原理以及对象与属性赋值

1、定义时间原理 2、定义对象与对象与属性赋值 ps:赋值过程其实会通过魔术方法_get调动本类的set方法 来源B站大佬视频 4.Yii2.0 Advanced Object与Property的关系_哔哩哔哩_bilibili