当前位置: 首页 > news >正文

Java拓扑排序-解决树排序问题

SortedUtil — 拓扑排序工具类

按父子依赖关系对列表进行稳定排序,保证子节点排在父节点之后,且保持同层兄弟节点的原始相对顺序。


快速开始

// 1. 定义你的数据结构,提供 id 和 parentId 的 getterList<YourEntity>list=...;// 2. 调用排序List<YourEntity>sorted=SortedUtil.topologicalSortByParent(list,YourEntity::getId,YourEntity::getParentId);

核心方法

publicstatic<T,ID>List<T>topologicalSortByParent(List<T>items,Function<T,ID>idGetter,Function<T,ID>parentGetter)

参数

参数类型说明
itemsList<T>原始列表,允许包含重复 id 的元素,允许 null
idGetterFunction<T, ID>提取元素唯一标识的函数
parentGetterFunction<T, ID>提取元素父标识的函数

返回值

排序后的新ArrayList<T>,原始列表不会被修改。

异常

异常条件
IllegalStateException检测到循环依赖(含自环),消息中会列出涉及的节点 id

排序规则

  1. 父在前,子在后— DFS 前序遍历保证每个节点的所有子节点紧跟在它之后
  2. 同层按原始顺序— 同级兄弟节点按在列表中首次出现的先后顺序排列
  3. 重复 id— 相同 id 的元素在组内保持原始相对顺序
  4. 多根支持— 存在多个根节点时,按根节点首次出现顺序依次处理
  5. 孤立父节点— 若某个节点的 parentId 不在列表中或为 null,该节点视为根

算法原理

四步流程

输入列表 │ ▼ [1. 分组] ─── 按 id 分组,记录首次下标 │ ▼ [2. 建图] ─── 构建 parent → [children] 邻接表 │ ▼ [3. 找根] ─── parentId 不在列表中的节点作为根 │ ▼ [4. DFS] ─── 从根开始前序遍历,三色标记检测环 │ ▼ 输出结果

三色标记法环检测

颜色含义说明
WHITE未访问节点尚未被 DFS 遍历
GRAY访问中正在遍历该节点(在递归栈上),若再次遇到说明有环
BLACK已结束该节点及其所有子节点都已遍历完毕

环检测触发条件(二选一):

  • 遍历中:DFS 访问子节点时遇到 GRAY 节点 → 环
  • 遍历后:所有根节点处理完毕,仍有节点为 WHITE → 孤岛环(不可达)

完整源码

packagecom.yourpackage;importjava.util.*;importjava.util.function.Function;importjava.util.stream.Collectors;publicclassSortedUtil{/** * 拓扑排序:按父子依赖关系稳定排序,子节点在父节点之后 * * @param items 原始列表(可能有重复 id) * @param idGetter 提取元素 id 的函数 * @param parentGetter 提取元素 parentId 的函数 * @param <T> 元素类型 * @param <ID> id 类型 * @return 排序后的新列表 */publicstatic<T,ID>List<T>topologicalSortByParent(List<T>items,Function<T,ID>idGetter,Function<T,ID>parentGetter){if(items==null||items.isEmpty())returnnewArrayList<>();// 1. 按 id 分组(保持首次出现顺序)Map<ID,List<T>>groupById=newLinkedHashMap<>();Map<ID,Integer>firstIndex=newLinkedHashMap<>();for(inti=0;i<items.size();i++){Titem=items.get(i);IDid=idGetter.apply(item);groupById.computeIfAbsent(id,k->newArrayList<>()).add(item);firstIndex.putIfAbsent(id,i);}Set<ID>allIds=groupById.keySet();// 2. 构建 parent -> children 邻接表Map<ID,Set<ID>>children=newHashMap<>();for(IDid:allIds){children.put(id,newLinkedHashSet<>());}for(Titem:items){IDparentId=parentGetter.apply(item);if(parentId!=null&&allIds.contains(parentId)){children.get(parentId).add(idGetter.apply(item));}}// 3. 找根节点(parentId 不在 allIds 中或为 null)List<ID>roots=allIds.stream().filter(id->{IDparentId=parentGetter.apply(groupById.get(id).get(0));returnparentId==null||!allIds.contains(parentId);}).sorted(Comparator.comparingInt(firstIndex::get)).collect(Collectors.toList());// 4. DFS 前序遍历(三色标记法检测环)Map<ID,Color>color=newHashMap<>();for(IDid:allIds){color.put(id,Color.WHITE);}List<ID>sortedIds=newArrayList<>();for(IDroot:roots){dfs(root,children,color,sortedIds);}// 5. 环检测:仍为 WHITE 的节点形成环for(IDid:allIds){if(color.get(id)==Color.WHITE){thrownewIllegalStateException("检测到循环依赖,涉及节点: "+id);}}// 6. 按排序后的 id 展开组内元素(保持原始顺序)List<T>result=newArrayList<>(items.size());for(IDid:sortedIds){result.addAll(groupById.get(id));}returnresult;}enumColor{WHITE,GRAY,BLACK}privatestatic<ID>voiddfs(IDid,Map<ID,Set<ID>>children,Map<ID,Color>color,List<ID>sortedIds){if(color.get(id)==Color.GRAY){thrownewIllegalStateException("检测到循环依赖,涉及节点: "+id);}if(color.get(id)==Color.BLACK){return;}color.put(id,Color.GRAY);sortedIds.add(id);for(IDchildId:children.get(id)){dfs(childId,children,color,sortedIds);}color.put(id,Color.BLACK);}}

边界情况

场景行为
null输入返回空列表
空列表返回空列表
单节点直接返回
所有节点均为根保持原始顺序
子节点先于父节点出现父节点仍排在前面(按 id 分组后从根开始遍历)
父节点不在列表中该节点被当作根节点
重复 id根据 id 分组,组内保持原始顺序
循环依赖 / 自环IllegalStateException
DAG(非严格树,如菱形)能正确处理,已经访问的节点不会重复遍历

复杂度

指标
时间复杂度O(V + E)
空间复杂度O(V)

其中 V 为唯一 id 数量,E 为父子关系边数。


使用示例

示例 1:机构树排序

List<Org>orgs=Arrays.asList(newOrg("总部",null),newOrg("研发部","总部"),newOrg("前端组","研发部"),newOrg("后端组","研发部"),newOrg("市场部","总部"));List<Org>sorted=SortedUtil.topologicalSortByParent(orgs,Org::getCode,Org::getParentCode);// 结果: 总部 → 研发部 → 前端组 → 后端组 → 市场部

示例 2:带重复数据的非法组织

List<Org>orgs=Arrays.asList(newOrg("总部",null),newOrg("研发部","总部"),newOrg("研发部","总部"),// 重复newOrg("前端组","研发部"));List<Org>sorted=SortedUtil.topologicalSortByParent(orgs,Org::getCode,Org::getParentCode);// 结果: 总部 → 研发部 → 研发部(重复) → 前端组
http://www.jsqmd.com/news/1139167/

相关文章:

  • SLC/MLC/TLC/QLC 闪存颗粒:从 1bit 到 4bit 的寿命与速度量化对比
  • 充电宝LED灯PCB布局实战:从7颗IC布局看EMI与热设计3要点
  • 基恩士SensofarZygo之外,国产轮廓仪如何选
  • 从零实现一个容器运行时:Docker的核心设计
  • comfyUI 安装流程:
  • 城市美学视角:科技融于山水,叁仟智慧灯杆重塑杭州夜间城市风貌
  • 7 种方法将三星S26/S25/S24/S23手机视频传电脑
  • 批量生成文章的字数失控与无配图问题,我的临时 workaround
  • 多个内网服务怎么统一管理?用Nginx Proxy Manager搭建HTTPS入口
  • 内网渗透第一步——信息收集决定了你能走多远
  • Day15 SpringIOC容器启动全流程:从ApplicationContext到Bean实例化
  • 触觉引导模型选型指南:环境适配四维解构法
  • VisualCppRedist AIO:3步彻底终结Windows软件运行库安装难题
  • 机械设计形位公差避坑:基准统一与5种常见测量方法对应关系
  • 天落地 GLM-5.2:昇腾 910B 双机分布式推理保姆级教程(纯实操命令,全文约 4000 字)
  • 南京南德管理|组织效能培训2026企业高频FAQ问答
  • 网页资源批量下载终极方案:告别繁琐,一键搞定所有文件
  • 汽车雨刷喷水电机故障诊断与DIY维修指南
  • 卡特加特是一家人工智能公司吗
  • 无创血液检测预警老年远期死亡:四种神经退行性标志物谁更具预测优势?
  • TwinCAT 3 配置汇川 IS620N 伺服:5个关键 COE 启动参数详解与避坑指南
  • 百考通答辩PPT生成:智能辅助,让学术答辩从容自信,专业而不失个性
  • Polars 性能碾压 Pandas 吗?千万级真实业务数据实测给出答案
  • 宝可梦数据集在军事AI训练中的应用与合规分析
  • PCB 工艺参数实战指南:从 0.3mm 过孔到 0.16mm 线距的 7 项设计避坑清单
  • 零基础实战:基于 PaddleVideo 与 PP-TSM 的自定义视频分类全流程指南
  • 济南AI智能体公司怎么选?从价格、案例到交付能力的完整选型手册
  • 通过 pip 直接安装免费版本:
  • 2026年下半年AI量化提效,先说清真正问题
  • 企业软件选型实操手册:规避开发风险,锁定优质交付